
/* ==== packages/ui/tokens.css ==== */
/* ============================================================
   packages/ui/tokens.css — الرموز التصميمية
   ============================================================
   The single source of truth for colour, spacing, radius and type.
   Loaded first, before every component sheet.

   Colour, spacing and radius are a byte-exact port of DESIGN-wise.md and
   must not drift. The type scale and the density rules are the documented
   product extension — see docs/DESIGN-product.md for why they exist.
   ============================================================ */

/* ---------------------------------------------------------- fonts */

/* Two families, split by unicode-range.
   DESIGN-wise.md mandates Inter. Inter has NO Arabic glyphs, and this UI is
   ~95% Arabic — shipping it alone would style the digits and nothing else,
   which is why the spec needed extending rather than merely obeying.
   Noto Sans Arabic carries the Arabic, is variable 400–900 (so Arabic can
   actually reach the mandated weight 900, which IBM Plex Arabic tops out
   below at 700), and is metrically calm enough to sit beside Inter.
   Both are self-hosted: the CSP is font-src 'self' and there is no CDN. */
@font-face{
  font-family:"Inter";
  font-style:normal;
  font-weight:400 900;
  font-display:swap;
  src:url("Inter-latin.c9407645.woff2") format("woff2");
  unicode-range:U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA,
                U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122,
                U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face{
  font-family:"Noto Sans Arabic";
  font-style:normal;
  font-weight:400 900;
  font-display:swap;
  src:url("NotoSansArabic-var.69cdf0bf.woff2") format("woff2");
  unicode-range:U+0600-06FF, U+0750-077F, U+0870-088E, U+0890-0891, U+0897-08E1,
                U+08E3-08FF, U+200C-200E, U+2010-2011, U+204F, U+2E41,
                U+FB50-FDFF, U+FE70-FE74, U+FE76-FEFC;
}

:root{
  /* ---- colour — DESIGN-wise.md, exact ---- */
  --primary:#9fe870; --on-primary:#0e0f0c; --primary-active:#cdffad;
  --primary-neutral:#c5edab; --primary-pale:#e2f6d5;
  --ink:#0e0f0c; --ink-deep:#163300; --body:#454745; --mute:#868685;
  --canvas:#ffffff; --canvas-soft:#e8ebe6;
  --positive:#2ead4b; --positive-deep:#054d28;
  --warning:#ffd11a; --warning-deep:#b86700; --warning-content:#4a3b1c;
  --negative:#d03238; --negative-deep:#a72027; --negative-darkest:#a7000d; --negative-bg:#320707;
  --accent-orange:#ffc091; --accent-cyan:#38c8ff;

  /* ---- derived surfaces — product extension.
         Previously raw hex in shell.css; mixed from the tokens so a palette
         change reaches them. ---- */
  --accent-cyan-bg:color-mix(in srgb, var(--accent-cyan) 18%, var(--canvas));
  --accent-cyan-ink:#04506b;
  --accent-orange-bg:color-mix(in srgb, var(--accent-orange) 32%, var(--canvas));
  --accent-orange-ink:#8a4b1c;
  --line:color-mix(in srgb, var(--ink) 12%, var(--canvas));
  --line-soft:var(--canvas-soft);
  --shadow-1:0 1px 2px rgba(14,15,12,.06);
  --shadow-2:0 6px 20px rgba(14,15,12,.10);
  --shadow-3:0 16px 40px rgba(14,15,12,.12);

  /* ---- radius — DESIGN-wise.md, exact. 24px (xl) is the canonical button
         and card radius and is NOT reduced on small screens. ---- */
  --r-none:0; --r-sm:8px; --r-md:12px; --r-lg:16px; --r-xl:24px;
  --r-pill:9999px; --r-full:9999px;

  /* ---- spacing — 4px base, DESIGN-wise.md, exact ---- */
  --s-xxs:2px; --s-xs:4px; --s-sm:8px; --s-md:12px; --s-lg:16px;
  --s-xl:24px; --s-2xl:32px; --s-3xl:48px;

  /* ---- type — product scale.
         DESIGN-wise.md's ladder runs to 126px for marketing heroes; this is a
         dense operational app and never uses anything above 40. Seven steps,
         and only two weights: 900 for hero and KPI values, 600 for the rest.
         Nothing in between — that rule is what kept ~20 ad-hoc sizes and a
         sprawl of 700/800 weights out of the product. ---- */
  --t-cap:12px;    --t-cap-lh:16px;
  --t-sm:14px;     --t-sm-lh:20px;
  --t-body:16px;   --t-body-lh:24px;
  --t-lg:20px;     --t-lg-lh:30px;
  --t-h3:20px;     --t-h3-lh:26px;
  --t-h2:24px;     --t-h2-lh:31px;
  --t-h1:32px;     --t-h1-lh:38px;
  --t-display:40px;--t-display-lh:44px;
  --w-normal:400; --w-strong:600; --w-hero:900;

  --font:"Inter","Noto Sans Arabic",system-ui,-apple-system,sans-serif;

  /* ---- motion ----
     One curve, three durations. Before these existed the sheets carried
     eight hand-typed values between .1s and .65s, which is one convention
     too many for a system with a single easing token.

     fast = state feedback you should barely notice (hover, press, a chip)
     mid  = something entering or leaving (a row, a sheet, an accordion)
     slow = something telling a story (a bar growing, a queue row resolved)

     Everything here is neutralised by the prefers-reduced-motion block in
     app.css. JS-driven motion must check REDUCED itself — that block only
     reaches CSS, not a requestAnimationFrame loop. */
  --ease:cubic-bezier(.22,.61,.36,1);
  --d-fast:.14s;
  --d-mid:.22s;
  --d-slow:.42s;

  /* ---- v2 motion: a spring for what pops in (a sheet, a toast, a press
         released), a long ease-out for what settles (a list entering). Stagger
         is per item, capped in motion.js so a long list never makes anyone
         wait. All of it is neutralised by the reduced-motion block. ---- */
  --ease-spring:cubic-bezier(.34,1.56,.64,1);
  --ease-out:cubic-bezier(.16,1,.3,1);
  --d-enter:.32s;
  --d-stagger:40ms;

  /* ---- v2 depth — the "3D" is light, not geometry.
         Every surface is lit from above: a 1px highlight on its top edge, a
         tight contact shadow under it, and a wide soft one further out. A
         pressed thing loses its outer shadow and gains an inner one. Pure
         box-shadow and gradient, so it costs nothing on a cheap phone and
         nothing to print (the print sheet drops them). Every colour is mixed
         from the palette. ---- */
  --depth-1:0 1px 1px color-mix(in srgb, var(--ink) 5%, transparent),
            0 2px 6px color-mix(in srgb, var(--ink) 7%, transparent);
  --depth-2:0 1px 2px color-mix(in srgb, var(--ink) 6%, transparent),
            0 6px 14px color-mix(in srgb, var(--ink) 9%, transparent),
            0 16px 32px color-mix(in srgb, var(--ink) 6%, transparent);
  --depth-3:0 2px 4px color-mix(in srgb, var(--ink) 8%, transparent),
            0 12px 24px color-mix(in srgb, var(--ink) 12%, transparent),
            0 28px 56px color-mix(in srgb, var(--ink) 10%, transparent);
  --depth-press:inset 0 2px 5px color-mix(in srgb, var(--ink) 20%, transparent);
  --hi-edge:inset 0 1px 0 color-mix(in srgb, var(--canvas) 92%, transparent);
  /* the underside of a raised key — a button is a thing with a thickness */
  --key-edge:0 3px 0 color-mix(in srgb, var(--primary) 62%, var(--ink-deep));
  --key-edge-press:0 1px 0 color-mix(in srgb, var(--primary) 62%, var(--ink-deep));
  --glass:color-mix(in srgb, var(--canvas) 80%, transparent);
  --grad-primary:linear-gradient(180deg,
                    color-mix(in srgb, var(--primary-active) 70%, var(--primary)) 0%,
                    var(--primary) 100%);
  --grad-ink:linear-gradient(160deg, color-mix(in srgb, var(--ink-deep) 70%, var(--ink)) 0%, var(--ink) 100%);
  --grad-surface:linear-gradient(180deg, var(--canvas) 0%,
                    color-mix(in srgb, var(--canvas) 96%, var(--canvas-soft)) 100%);

  /* ---- the sign-in surface — product extension, DESIGN-product.md §2.8 ----
     The one screen that is a brand moment rather than a work surface. Every
     colour is mixed from the palette above, so nothing drifts: the sun
     behind the palm takes the hour's tint, and the lattice is lime at a
     whisper. --t-hero is the only size above 40px anywhere in the product,
     and only the wordmark on the ink band uses it. */
  --auth-glow-dawn:color-mix(in srgb, var(--accent-orange) 70%, var(--primary));
  --auth-glow-day:var(--primary);
  --auth-glow-dusk:color-mix(in srgb, var(--accent-orange) 78%, var(--warning));
  --auth-glow-night:color-mix(in srgb, var(--accent-cyan) 55%, var(--primary));
  --auth-line:color-mix(in srgb, var(--primary) 13%, transparent);
  --auth-veil:color-mix(in srgb, var(--canvas) 7%, transparent);
  --t-hero:64px;

  /* ---- layout ----
     The page gutter was 16px on a phone and 24px from 600 up, inside a column
     capped at 1320px: on a tablet or a desktop a third of the screen was
     margin. v2: 12px on a phone, 16px on a tablet, and on a desktop it grows
     only as the screen does (2vw, 16–32px) with no cap on the column. */
  --pad:var(--s-md);
  --pad-wide:clamp(16px, 2vw, 32px);
  --card-pad:var(--s-lg);
  --rail-w:76px;
  --topbar:56px;
  --toast-top:calc(var(--sat) + var(--topbar) + 8px);
  /* --tabbar is what everything ABOVE the bottom edge measures itself
     against — the floating action button, the bulk bar, the toast host all
     read calc(var(--tabbar) + var(--sab) + …). It stays 0px here because the
     platform shell, the sign-in screen and لوحة الإدارة genuinely have no
     bar; an app that renders one adds body.has-mtabs, which redefines this
     inside the breakpoint, and the three of them move for free. */
  --tabbar:0px;
  --tabbar-h:64px;         /* the height of an app-owned bottom bar */
  --fab:56px;              /* the floating action button, and the room it needs */
  --touch:44px;            /* minimum hit target — product law */
  --sat:env(safe-area-inset-top,0px);
  --sab:env(safe-area-inset-bottom,0px);

  /* ---- variety series — fourteen fills for the fridge map ----
     Declared here because the design-token test slices from `:root{` to its
     closing brace and fails the build on a raw hex anywhere outside it.

     The brand green is ABSENT BY LAW. --primary means "an action you can
     take"; a rack full of عجوة is not an action, and the one green on the
     map is the slot you have just chosen.

     Row A is the eight categorical bases the charts already sanction. Row B
     is the same hues mixed toward the canvas, which gives a second, lighter
     ring that stays distinguishable from its parent. color-mix is already
     proven in this file (--accent-cyan-bg, --line).

     Beyond about seven hues nobody tells them apart reliably — least of all
     a colour-blind person under one bulb in a cold store — so slots 8-14 also
     carry a diagonal hatch in fridge-map.css. Hue plus texture survives what
     hue alone does not. */
  --v-1:var(--ink-deep);
  --v-2:var(--accent-cyan);
  --v-3:var(--accent-orange);
  --v-4:var(--positive);
  --v-5:var(--warning);
  --v-6:var(--negative);
  --v-7:var(--body);
  --v-8: color-mix(in srgb, var(--ink-deep)      45%, var(--canvas));
  --v-9: color-mix(in srgb, var(--accent-cyan)   42%, var(--canvas));
  --v-10:color-mix(in srgb, var(--accent-orange) 58%, var(--canvas));
  --v-11:color-mix(in srgb, var(--positive)      40%, var(--canvas));
  --v-12:color-mix(in srgb, var(--warning)       46%, var(--canvas));
  --v-13:color-mix(in srgb, var(--negative)      36%, var(--canvas));
  --v-14:color-mix(in srgb, var(--body)          38%, var(--canvas));

  /* the two states that are not a variety */
  --v-free:var(--canvas-soft);
  --v-blocked:color-mix(in srgb, var(--mute) 30%, var(--canvas));
}

@media (min-width:600px){
  :root{ --pad:var(--s-lg); --card-pad:20px; }
}
@media (min-width:900px){
  :root{ --pad:var(--pad-wide); }
}


/* ==== packages/ui/app.css ==== */
/* ============================================================
   packages/ui/app.css — مفردات المكوّنات
   ============================================================
   Components only. Tokens live in tokens.css, which loads first.
   ============================================================ */

*,*::before,*::after{box-sizing:border-box}
html{-webkit-text-size-adjust:100%}
html,body{margin:0;padding:0;max-width:100%}
body{
  font-family:var(--font); font-size:16px; line-height:24px; font-weight:400;
  color:var(--ink); background:var(--canvas-soft);
  overscroll-behavior-y:none;
}
button,input,select,textarea{font:inherit;color:inherit}
button{cursor:pointer;border:none;background:none;text-align:inherit}
h1,h2,h3,h4{margin:0;font-weight:900;letter-spacing:-.01em}
bdi,.num{unicode-bidi:isolate;direction:ltr;font-variant-numeric:tabular-nums}
img,svg{max-width:100%}
.hide{display:none !important}
:focus-visible{outline:3px solid var(--positive);outline-offset:2px;border-radius:var(--r-sm)}

/* ---------- typography (fluid) ---------- */
.t-display{font-size:var(--t-display);line-height:var(--t-display-lh);font-weight:var(--w-hero);letter-spacing:-.02em}
.t-h1{font-size:var(--t-h1);line-height:var(--t-h1-lh);font-weight:var(--w-hero);letter-spacing:-.02em}
.t-h2{font-size:var(--t-h2);line-height:var(--t-h2-lh);font-weight:var(--w-strong)}
.t-h3{font-size:var(--t-h3);line-height:var(--t-h3-lh);font-weight:var(--w-strong)}
.t-body{font-size:var(--t-body);line-height:var(--t-body-lh);font-weight:var(--w-normal)}
.t-sm{font-size:var(--t-sm);line-height:var(--t-sm-lh);font-weight:var(--w-normal)}
.t-sm-b{font-size:var(--t-sm);line-height:var(--t-sm-lh);font-weight:var(--w-strong)}
.t-cap{font-size:var(--t-cap);line-height:var(--t-cap-lh);font-weight:var(--w-normal)}
.mute{color:var(--mute)} .sec{color:var(--body)}
.ell{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;min-width:0}

/* ============================================================
   Layout — mobile first (320px baseline)
   ============================================================ */
.wrap{width:100%;margin:0 auto;padding-inline:var(--pad)}   /* v2: no cap — the gutter alone frames the page */
.stack{display:flex;flex-direction:column;gap:var(--s-lg);min-width:0}
.stack.sm{gap:var(--s-sm)} .stack.md{gap:var(--s-md)}
.row{display:flex;align-items:center;gap:var(--s-sm);flex-wrap:wrap;min-width:0}
/* الجذر الحقيقي لانزلاق الصفحة: flex:1 يضبط الأساس صفراً ويترك
   min-width:auto، فالحدّ الأدنى لكل زرّ هو عرض نصّه كاملاً بلا التفاف —
   ولا يستطيع flex أن يُصغّره دون ذلك مهما ضاقت الشاشة. */
/* قسمٌ يُعاد رسمه ليس قسماً يُركَّب لأول مرة.
   حركات الدخول تُطفأ أثناء إعادة الرسم؛ العدّادات والأشرطة تفعل المثل من
   جافاسكربت عبر isRepainting(). */
[data-repaint] *{ animation:none !important }
.row.nowrap{flex-wrap:nowrap}
.row > .btn{min-width:0}
.row > .btn > span{overflow:hidden;text-overflow:ellipsis}
.between{display:flex;align-items:center;justify-content:space-between;gap:var(--s-md);flex-wrap:wrap;min-width:0}
.grid{display:grid;gap:var(--s-md)}
.g2,.g3,.g4{grid-template-columns:1fr}              /* everything stacks by default */
.spacer{flex:1}
.sep{height:1px;background:var(--canvas-soft);margin-block:var(--s-md);border:0}

/* 600, not 450.
   Two cards side by side on a phone gives each about 200px, which is not a
   card, it is a column of wrapped words. 450 was also the one number in this
   file that was not on the ladder the rest of the system uses — 400, 600,
   768, 900, 1024 — so it was both wrong and unexplained. The dashboard's six
   breakdown cards are all .g2 rows, so this one line settles all of them. */
@media(min-width:600px){ .g2{grid-template-columns:repeat(2,1fr)} }
@media(min-width:600px){ .g4{grid-template-columns:repeat(2,1fr)} }
@media(min-width:768px){ .grid{gap:var(--s-lg)} }
@media(min-width:1024px){ .g4{grid-template-columns:repeat(4,1fr)} }
@media(min-width:900px){ .g3{grid-template-columns:repeat(3,1fr)} }
/* صف جرد تجاوز الحد المسموح: شريط لا تعبئة، ليبقى مقروءاً على الورق */
tbody tr.row-flag td:first-child{box-shadow:inset 3px 0 0 var(--negative)}

/* ---------- cards ---------- */
.card{background:var(--canvas);border-radius:var(--r-xl);padding:var(--card-pad)}
.card-sage{background:var(--canvas-soft);border-radius:var(--r-lg);padding:var(--s-lg)}
.card-green{background:var(--primary-pale);border-radius:var(--r-lg);padding:var(--s-lg)}
.card-dark{background:var(--ink);color:var(--primary);border-radius:var(--r-lg);padding:var(--s-lg)}
.card.flush{padding:0;overflow:hidden}
@media(min-width:600px){
  .card,.card-sage,.card-green,.card-dark{border-radius:var(--r-xl);padding:var(--card-pad)}
  .card.tight{padding:var(--s-lg)}
}
/* v2 — a card is a lit surface: a highlight on its top edge, a contact shadow
   and a soft one. `.lift` makes it answer a finger (it sinks) and a pointer
   (it rises, and tilts toward it — motion.js sets --rx/--ry). */
.card{background:var(--grad-surface);box-shadow:var(--hi-edge),var(--depth-1)}
.card-dark{
  background:radial-gradient(70% 90% at 100% 0%, color-mix(in srgb, var(--primary) 22%, transparent), transparent 72%),
             var(--grad-ink);
  box-shadow:var(--hi-edge),var(--depth-2)}
.card-sage,.card-green{box-shadow:var(--hi-edge)}
.card.flush{box-shadow:var(--depth-1)}
.card.lift,.list-card.as-btn,.kpi-tap{
  transition:transform var(--d-mid) var(--ease-out),box-shadow var(--d-mid) var(--ease-out)}
.card.lift,.list-card.as-btn{
  transform:perspective(900px) rotateX(var(--rx,0deg)) rotateY(var(--ry,0deg)) translateZ(0)}
.card.lift:active,.list-card.as-btn:active{
  transform:scale(.985);box-shadow:var(--hi-edge),var(--depth-press)}
@media(hover:hover) and (pointer:fine){
  .card.lift:hover,.list-card.as-btn:hover{box-shadow:var(--hi-edge),var(--depth-2)}
}

/* ---------- buttons ---------- */
.btn{display:inline-flex;align-items:center;justify-content:center;gap:var(--s-sm);
  font-size:15px;line-height:20px;font-weight:600;border-radius:var(--r-xl);
  padding:var(--s-md) var(--s-lg);min-height:48px;
  transition:background .14s var(--ease),filter .14s var(--ease),transform .1s var(--ease);
  white-space:nowrap;max-width:100%}
@media(min-width:600px){ .btn{font-size:16px;line-height:24px;padding:var(--s-md) var(--s-xl)} }
.btn:active{transform:scale(.97)}
/* v2 — the primary button is a key: it has a thickness (--key-edge) and it
   sinks into it when pressed, instead of merely shrinking. */
.btn-p{background:var(--grad-primary);color:var(--on-primary);
  box-shadow:var(--hi-edge),var(--key-edge),var(--depth-1);margin-block-end:3px;
  transition:background .14s var(--ease),filter .14s var(--ease),
             transform .1s var(--ease),box-shadow .1s var(--ease),margin .1s var(--ease)}
.btn-p:hover{background:var(--primary-active)}
.btn-p:active{transform:translateY(2px);margin-block-end:1px;box-shadow:var(--key-edge-press)}
.btn-s{background:var(--canvas-soft);color:var(--ink);box-shadow:var(--hi-edge)}
.btn-s:hover{filter:brightness(.96)}
.btn-t{background:var(--canvas);color:var(--ink);box-shadow:inset 0 0 0 1px var(--ink),var(--depth-1)}
.btn-t:hover{background:var(--canvas-soft)}
.btn-d{background:var(--canvas);color:var(--negative-darkest);box-shadow:inset 0 0 0 1px var(--negative),var(--depth-1)}
.btn-d:hover{background:var(--negative);color:var(--canvas)}
.btn-sm{min-height:40px;padding:var(--s-sm) var(--s-lg);font-size:14px;border-radius:var(--r-pill)}
/* زر أيقونة — خلفية بيضاء + حد رفيع ليبقى مرئياً فوق الخلفية الرمادية */
.btn-icon{width:46px;height:46px;min-height:46px;padding:0;border-radius:var(--r-pill);
  background:var(--canvas);box-shadow:inset 0 0 0 1px var(--canvas-soft);
  display:inline-grid;place-items:center;flex:none;font-size:17px;
  transition:background .14s var(--ease),box-shadow .14s var(--ease)}
.btn-icon:hover{background:var(--canvas-soft);box-shadow:inset 0 0 0 1px var(--mute)}
.btn-icon.dirty{background:var(--warning);box-shadow:inset 0 0 0 1px var(--warning-deep)}
/* زر «المزيد» داخل بطاقة الطبلية — هدف لمس كافٍ */
/* 48px wide and 40px tall: wide enough to look like a target, four pixels
   short of being one. It is the ⋯ that opens every per-pallet action on the
   mobile card list. */
.more-btn{min-width:48px;height:40px;min-height:var(--touch);border-radius:var(--r-pill);background:var(--canvas-soft);
  display:inline-grid;place-items:center;font-size:16px;font-weight:var(--w-strong);color:var(--body);
  margin-inline-start:auto;flex:none}
.more-btn:hover{background:var(--ink);color:var(--primary)}
.btn-block{width:100%}
.btn[disabled]{opacity:.4;pointer-events:none}

/* ---------- badges & pills ---------- */
.badge{display:inline-flex;align-items:center;gap:4px;border-radius:var(--r-pill);
  padding:var(--s-xs) var(--s-md);font-size:12px;line-height:16px;font-weight:600;white-space:nowrap;max-width:100%}
.badge.ell{display:inline-block}
.b-pos{background:var(--primary-pale);color:var(--positive-deep)}
.b-warn{background:var(--warning);color:var(--warning-content)}
.b-neg{background:var(--negative-bg);color:var(--canvas)}
.b-neutral{background:var(--canvas-soft);color:var(--body)}
.b-ink{background:var(--ink);color:var(--primary)}
.dot{width:8px;height:8px;border-radius:50%;background:currentColor;flex:none}
.swatch{width:10px;height:10px;border-radius:3px;flex:none;display:inline-block}

/* ---------- chips ---------- */
.chips{display:flex;gap:var(--s-sm);flex-wrap:wrap}
.chips.scroll{flex-wrap:nowrap;overflow-x:auto;padding-bottom:4px;
  scrollbar-width:none;-webkit-overflow-scrolling:touch}
.chips.scroll::-webkit-scrollbar{display:none}
.chip{background:var(--canvas);box-shadow:inset 0 0 0 1px var(--canvas-soft);border-radius:var(--r-pill);
  padding:var(--s-sm) var(--s-md);font-size:14px;font-weight:600;min-height:var(--touch);min-width:var(--touch);justify-content:center;
  display:inline-flex;align-items:center;gap:6px;white-space:nowrap;flex:none;
  transition:background .14s var(--ease),box-shadow .14s var(--ease)}
.chip.on{background:var(--primary);box-shadow:inset 0 0 0 1px var(--primary),var(--depth-1);color:var(--on-primary)}
.chip.on-dark{background:var(--ink);box-shadow:inset 0 0 0 1px var(--ink);color:var(--primary)}
/* زر حذف داخل الشريحة — هدف لمس كافٍ على الجوال */
.chip .x{width:40px;height:40px;margin-inline:6px -8px;border-radius:var(--r-pill);
  display:inline-grid;place-items:center;color:var(--negative-darkest);font-weight:900;font-size:16px}
.chip .x:hover{background:var(--negative);color:var(--canvas)}
/* 40px was below the floor, and the ✕ inside a settings chip is how a whole
   variety gets removed — the smallest target on the screen doing the most
   destructive thing on it. */
@media(max-width:899px){ .chip{min-height:44px} .chip .x{width:44px;height:44px} }
/* An app that renders a bottom bar declares it here rather than in four
   stylesheets. Desktop never matches, so every calc() collapses to today's
   value and there is no desktop diff at all. */
@media(max-width:899px){ body.has-mtabs{--tabbar:var(--tabbar-h)} }
@media(min-width:900px){ .chip .x{width:28px;height:28px;margin-inline:4px -6px} }
.seg{display:inline-flex;background:var(--canvas-soft);border-radius:var(--r-pill);padding:4px;gap:2px;max-width:100%;overflow-x:auto;
  scrollbar-width:none}
.seg::-webkit-scrollbar{display:none}
.seg button{border-radius:var(--r-pill);padding:var(--s-sm) var(--s-md);font-size:14px;font-weight:600;min-height:40px;
  white-space:nowrap;flex:none;transition:background .14s var(--ease)}
.seg button.on{background:var(--canvas)}

/* ============================================================
   Desktop chrome
   ============================================================ */
header.nav{position:sticky;top:0;z-index:40;background:var(--canvas);border-bottom:1px solid var(--canvas-soft)}
.nav-in{margin:0 auto;padding:var(--s-md) var(--pad);display:flex;align-items:center;gap:var(--s-lg)}
.brand{display:flex;align-items:center;gap:var(--s-sm);font-weight:900;font-size:18px;white-space:nowrap}
.brand .mark{width:32px;height:32px;border-radius:var(--r-sm);background:var(--primary);
  display:grid;place-items:center;font-size:18px;flex:none}
.nav-links{display:flex;gap:var(--s-xs)}
.nav-link{font-size:14px;line-height:20px;font-weight:600;color:var(--ink);
  padding:var(--s-sm) var(--s-lg);border-radius:var(--r-pill);transition:background .14s var(--ease)}
.nav-link:hover{background:var(--canvas-soft)}
.nav-link.on{background:var(--primary-pale)}


main{padding-block:var(--s-lg) var(--s-3xl);min-height:60vh}

/* ============================================================
   Mobile chrome
   ============================================================ */



.m-top .ttl{font-size:17px;font-weight:900;letter-spacing:-.01em;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;flex:1;min-width:0}
.m-main{padding-top:calc(var(--topbar) + var(--sat));
        padding-bottom:calc(var(--tabbar) + var(--sab) + 16px);min-height:100vh}
.m-main.has-fab{padding-bottom:calc(var(--tabbar) + var(--sab) + 88px)}
.m-main.has-fab2{padding-bottom:calc(var(--tabbar) + var(--sab) + 144px)}
.m-main.has-bulk{padding-bottom:calc(var(--tabbar) + var(--sab) + 150px)}





.m-tab.on .ic{background:var(--primary-pale)}
@media(min-width:360px){  }
/* الأزرار العائمة. الأساسي أسفل الكومة دائماً، فلا يتحرّك حين يظهر ثانٍ
   فوقه — زرٌّ يغيّر مكانه بحسب الشاشة زرٌّ يُضغط خطأً. */
.fabstack{position:fixed;inset-inline-end:16px;bottom:calc(var(--tabbar) + var(--sab) + 16px);
  z-index:48;display:flex;flex-direction:column-reverse;align-items:center;gap:var(--s-sm)}
.fab{width:var(--fab);height:var(--fab);border-radius:var(--r-pill);
  background:var(--primary);color:var(--on-primary);
  display:grid;place-items:center;font-size:26px;font-weight:var(--w-strong);
  box-shadow:0 6px 20px rgba(14,15,12,.22);transition:transform .18s var(--ease)}
/* الثانوي أصغر وأهدأ: دائرتان متطابقتان تجعلان الاختيار بينهما تخميناً.
   ولا يزال ٤٨ بكسلاً — فوق حدّ اللمس. */
.fab-2{width:48px;height:48px;font-size:22px;
  background:var(--canvas);color:var(--ink);
  box-shadow:0 4px 14px rgba(14,15,12,.18),inset 0 0 0 1px var(--canvas-soft)}
.fab:active{transform:scale(.92)}

/* ---------- page head ---------- */
.phead{padding-block:var(--s-md) var(--s-lg)}
.phead h1{font-size:clamp(24px,6.5vw,40px);line-height:1.15}
.phead .sub{margin:6px 0 0;color:var(--body)}

/* ---------- KPI ---------- */
.kpi .lab{font-size:12px;line-height:16px;font-weight:600;color:var(--body);
  display:flex;align-items:center;gap:6px}
.kpi .val{font-size:clamp(24px,7vw,38px);line-height:1.1;font-weight:900;margin-top:6px;letter-spacing:-.025em}
.kpi .unit{font-size:13px;font-weight:600;color:var(--mute);margin-inline-start:4px}
.kpi .sub{font-size:11px;line-height:15px;color:var(--mute);margin-top:6px}
.card-dark .kpi .lab,.card-dark .kpi .sub{color:var(--primary-neutral)}
.kpi-tap{display:block;width:100%;text-align:start;transition:transform .14s var(--ease)}
.kpi-tap:hover{transform:translateY(-2px)}
@media(min-width:1024px){ .kpi .val{font-size:38px} }

/* ---------- bars & charts ---------- */
.bar{height:10px;border-radius:var(--r-pill);background:var(--canvas-soft);overflow:hidden;display:flex}
.bar>i{display:block;height:100%;background:var(--primary);
  transition:width .65s var(--ease);will-change:width}
.bar.seg-bar>i{border-radius:0}
.bar.seg-bar>i:first-child{border-start-start-radius:var(--r-pill);border-end-start-radius:var(--r-pill)}
.bar.seg-bar>i:last-child{border-start-end-radius:var(--r-pill);border-end-end-radius:var(--r-pill)}
.chart-row{display:block;width:100%;text-align:start;padding:var(--s-sm);margin-inline:calc(var(--s-sm) * -1);
  border-radius:var(--r-md);transition:background .14s var(--ease)}
.chart-row:hover,.chart-row.hot{background:var(--canvas-soft)}
.legend{display:flex;flex-wrap:wrap;gap:8px 14px;margin-top:14px}
.legend span{display:inline-flex;align-items:center;gap:6px;font-size:12px;color:var(--body)}
.donut{display:block;margin-inline:auto;max-width:220px}
.donut path,.donut circle{transition:opacity .18s var(--ease),transform .18s var(--ease);transform-origin:50% 50%}
.donut .seg-arc{cursor:pointer}
.donut .seg-arc:hover,.donut .seg-arc.hot{opacity:.82}
.donut-mid{font-weight:900;text-anchor:middle}

/* ---------- tooltip ---------- */
#tips{position:fixed;inset:0;z-index:70;pointer-events:none}
.tip{position:absolute;max-width:min(280px,calc(100vw - 24px));background:var(--ink);color:var(--canvas);
  border-radius:var(--r-md);padding:var(--s-sm) var(--s-md);font-size:12px;line-height:17px;
  box-shadow:0 8px 24px rgba(14,15,12,.28);opacity:0;transform:translateY(4px);
  transition:opacity .14s var(--ease),transform .14s var(--ease)}
.tip.on{opacity:1;transform:translateY(0)}
.tip b{color:var(--primary);font-weight:var(--w-strong)}
.tip .tr{display:flex;justify-content:space-between;gap:14px;white-space:nowrap}
.tip .hd{font-weight:var(--w-hero);font-size:13px;margin-bottom:6px;color:var(--primary)}

/* ---------- tables ---------- */
.tbl-wrap{overflow-x:auto;-webkit-overflow-scrolling:touch;overscroll-behavior-x:contain}
table{width:100%;border-collapse:collapse;font-size:14px;line-height:20px}
thead th{background:var(--canvas-soft);font-size:12px;line-height:16px;font-weight:var(--w-strong);color:var(--body);
  text-align:start;padding:var(--s-md) var(--s-lg);white-space:nowrap;position:sticky;top:0;z-index:2}
thead th.sortable{cursor:pointer;user-select:none}
thead th.sortable:hover{color:var(--ink)}
tbody td{padding:var(--s-md) var(--s-lg);border-bottom:1px solid var(--canvas-soft);white-space:nowrap}
tbody tr{transition:background .12s var(--ease)}
tbody tr:hover{background:var(--primary-pale)}
tfoot td{padding:var(--s-md) var(--s-lg);font-weight:var(--w-hero);background:var(--canvas-soft);white-space:nowrap}
/* Desktop only: 34px is below the 44px touch floor, and the mobile card list
   uses an action sheet instead. Guarded so a future module cannot ship it to
   a phone by accident. */
.mini{display:none}
@media(min-width:900px){
.mini{display:inline-grid;width:34px;height:34px;min-height:34px;border-radius:var(--r-pill);background:var(--canvas-soft);
  display:inline-grid;place-items:center;font-size:14px;margin-inline-end:4px;
  transition:background .14s var(--ease)}
.mini:hover{background:var(--primary)}
.mini.del:hover{background:var(--negative);color:var(--canvas)}
}
/* heat-map pivot */
.pivot td.hc{text-align:center;font-weight:600}
.pivot th.rowh,.pivot td.rowh{position:sticky;inset-inline-start:0;z-index:1;
  background:var(--canvas);font-weight:var(--w-strong);box-shadow:1px 0 0 var(--canvas-soft)}
.pivot thead th.rowh{background:var(--canvas-soft);z-index:3}
.pivot tfoot td.rowh{background:var(--canvas-soft)}

/* ---------- inputs ---------- */
label.f{display:block;margin-bottom:var(--s-lg)}
label.f>span{display:block;font-size:13px;line-height:18px;font-weight:600;margin-bottom:6px}
.inp{width:100%;background:var(--canvas);color:var(--ink);
  box-shadow:inset 0 0 0 1px var(--ink),inset 0 2px 3px color-mix(in srgb, var(--ink) 6%, transparent);border:0;
  border-radius:var(--r-md);padding:var(--s-md) var(--s-lg);min-height:52px;outline:none;font-size:16px;
  transition:box-shadow .14s var(--ease)}
.inp:focus{box-shadow:inset 0 0 0 2px var(--positive),0 0 0 3px var(--primary-pale)}
.inp[readonly]{background:var(--canvas-soft);box-shadow:inset 0 0 0 1px var(--mute)}
textarea.inp{min-height:84px;resize:vertical;line-height:22px}
select.inp{appearance:none;
  background-image:linear-gradient(45deg,transparent 50%,var(--ink) 50%),linear-gradient(135deg,var(--ink) 50%,transparent 50%);
  background-position:14px 21px,20px 21px;background-size:6px 6px;background-repeat:no-repeat;padding-inline-start:34px}
.search{position:relative}
.search .inp{padding-inline-start:44px}
.search .ic{position:absolute;inset-inline-start:14px;top:50%;transform:translateY(-50%);color:var(--mute);pointer-events:none}
.err{color:var(--negative-darkest);font-size:12px;line-height:16px;font-weight:600;margin-top:4px}
/* ---------- the three validation tiers ----------
   إرشادي never blocks and carries no colour — colour on a hint teaches
   people to ignore colour. تحذير saves but must be acknowledged. حاجب
   refuses. Every blocking tier here mirrors a rule that genuinely exists in
   validate_document(); the client must not invent one the server lacks, or
   a legitimate document becomes unenterable. */
.hint-msg{color:var(--mute);font-size:12px;line-height:16px;margin-top:4px}
.warn-msg{color:var(--warning-deep);font-size:12px;line-height:16px;font-weight:600;margin-top:4px}
label.f.f-warn>.inp,label.f.f-warn>*>.inp{box-shadow:inset 0 0 0 2px var(--warning)}
label.f.f-block>.inp,label.f.f-block>*>.inp{box-shadow:inset 0 0 0 2px var(--negative)}
.warnbox{background:var(--warning);color:var(--warning-content);border-radius:var(--r-md);
  padding:var(--s-md) var(--s-lg);font-size:13px;line-height:19px;font-weight:600}
.switch{display:flex;align-items:center;gap:10px;cursor:pointer;min-height:48px}
.switch input{width:22px;height:22px;flex:none;accent-color:var(--positive)}

/* ============================================================
   Overlays — modal / bottom sheet / action sheet
   ============================================================ */
.ov{position:fixed;inset:0;z-index:60;background:rgba(14,15,12,.5);
  -webkit-backdrop-filter:blur(3px);backdrop-filter:blur(3px);
  display:flex;align-items:flex-end;justify-content:center;overflow:hidden;
  opacity:0;transition:opacity .2s var(--ease)}
.ov.on{opacity:1}
.modal{background:var(--canvas);width:100%;max-width:min(760px, 100vw);min-width:0;box-shadow:var(--shadow-3);
  border-radius:var(--r-xl) var(--r-xl) 0 0;max-height:100dvh;height:100dvh;
  display:flex;flex-direction:column;
  transform:translateY(6%);transition:transform var(--d-enter) var(--ease-spring)}
.ov.on .modal{transform:translateY(0)}
.modal.sheet{height:auto;max-height:88dvh;border-radius:var(--r-xl) var(--r-xl) 0 0}
@media(min-width:768px){
  .ov{align-items:center;padding:var(--s-xl)}
  .modal,.modal.sheet{border-radius:var(--r-xl);height:auto;max-height:88dvh;transform:scale(.97)}
  .ov.on .modal{transform:scale(1)}
}
/* The overlay must never pan sideways. `overflow-y:auto` alone is not enough:
   CSS computes an `overflow-x` of `visible` to `auto` as soon as the other
   axis is not visible, so a body that only asked to scroll vertically becomes
   a HORIZONTAL scroll container too, and one child a pixel too wide turns the
   whole dialog into a swipeable map. That is the bug this rule closes.
   Declared as `clip` for intent, though per css-overflow-3 it COMPUTES to
   `hidden` here, because a `clip` beside a non-clip, non-visible value on the
   other axis is defined to do exactly that. Both refuse the pan, and `hidden`
   costs nothing on this box: overflow-y:auto already made it a scroll
   container, so the position:sticky that .net-sticky needs is unaffected.
   Keeping `clip` in the source means the declaration stays correct if the
   vertical axis ever stops being auto.
   --modal-pad exists so .net-sticky's negative margin tracks the real padding
   instead of guessing it — see the note on that rule. */
.modal-h{padding:var(--s-lg) var(--s-lg) var(--s-sm);display:flex;align-items:flex-start;gap:var(--s-md);flex:none;min-width:0}
.modal-b{--modal-pad:var(--s-lg);
  padding:var(--s-sm) var(--modal-pad) var(--modal-pad);
  overflow-y:auto;overflow-x:clip;min-width:0;flex:1;
  -webkit-overflow-scrolling:touch;overscroll-behavior:contain}
/* A title is worth wrapping. Truncating it hides the variety name, which is
   the one word that tells عيسى which pallet he opened. */
.modal-h .sub,.modal-h .t-sm{overflow-wrap:anywhere}
/* Chips in a modal are labels, not a nav strip: a long custom variety or box
   name must wrap rather than push the dialog wide. .chips.scroll keeps its
   nowrap — that one really is a strip. */
.modal-b .chip,.card .chip{white-space:normal;text-align:start;max-width:100%}
.card .chips.scroll .chip{white-space:nowrap;max-width:none}
/* a choice the person must make, drawn as a full-width row: unselected is a
   bordered white surface, selected is lime with a tick — never the same
   colour as the card around it, which is what made a line look picked when
   it was not. */
.pick-list{display:flex;flex-direction:column;gap:var(--s-sm);min-width:0}
.pick-card{display:flex;align-items:center;gap:var(--s-md);width:100%;min-width:0;min-height:52px;
  padding:var(--s-md);border-radius:var(--r-lg);background:var(--canvas);
  box-shadow:inset 0 0 0 2px var(--canvas-soft);text-align:start;font-size:14px;font-weight:600;
  transition:background .14s var(--ease),box-shadow .14s var(--ease)}
.pick-card .pick-tick{flex:none;width:24px;height:24px;border-radius:50%;
  display:inline-flex;align-items:center;justify-content:center;
  box-shadow:inset 0 0 0 2px var(--canvas-soft);font-weight:900}
.pick-card .pick-text{min-width:0;overflow-wrap:anywhere}
.pick-card.on{background:var(--primary);box-shadow:inset 0 0 0 2px var(--primary);color:var(--on-primary)}
.pick-card.on .pick-tick{background:var(--on-primary);color:var(--primary);box-shadow:none}
.modal-b .chips.scroll .chip{white-space:nowrap}
.modal-f{padding:var(--s-md) var(--s-lg);border-top:1px solid var(--canvas-soft);flex:none;min-width:0;
  display:flex;gap:var(--s-md);background:var(--canvas);
  padding-bottom:calc(var(--s-md) + var(--sab))}
.modal-f .btn{flex:1;min-width:0}
@media(min-width:600px){ .modal-h{padding:var(--s-xl) var(--s-xl) var(--s-md)} .modal-b{--modal-pad:var(--s-xl);padding:0 var(--modal-pad) var(--modal-pad)} .modal-f{padding:var(--s-lg) var(--s-xl)} }
.grabber{width:40px;height:4px;border-radius:2px;background:var(--canvas-soft);margin:10px auto 0;flex:none}
@media(min-width:768px){ .grabber{display:none} }
/* sticky computed-net panel inside the form */
/* Bleeds to the dialog's edges, so its negative margin must cancel the body's
   padding EXACTLY. Hardcoding --s-lg was right below 600px and wrong above it,
   where the padding becomes --s-xl — 8px of scrollable width appearing at one
   breakpoint. Reading --modal-pad makes it correct at every width by
   construction rather than by coincidence. */
.net-sticky{position:sticky;bottom:0;z-index:2;margin-inline:calc(var(--modal-pad, var(--pad)) * -1);
  padding:var(--s-md) var(--modal-pad, var(--pad));background:var(--primary-pale);
  box-shadow:0 -6px 16px rgba(232,235,230,.9)}
@media(min-width:600px){ .net-sticky{position:static;margin-inline:0;border-radius:var(--r-lg)} }
.asheet .item{display:flex;align-items:center;gap:12px;width:100%;padding:var(--s-lg) var(--s-sm);
  font-size:16px;font-weight:600;border-radius:var(--r-md);min-height:56px}
.asheet .item:hover{background:var(--canvas-soft)}
.asheet .item.danger{color:var(--negative-darkest)}

/* ---------- misc ---------- */
.empty{text-align:center;padding:var(--s-3xl) var(--s-lg);background:var(--canvas-soft);border-radius:var(--r-xl)}
.empty .ic{font-size:40px}
.toast-host{position:fixed;inset-inline:0;bottom:calc(var(--tabbar) + var(--sab) + 12px);z-index:80;
  display:flex;flex-direction:column;align-items:center;gap:8px;pointer-events:none;padding-inline:var(--s-lg)}
@media(min-width:900px){ .toast-host{bottom:24px} }
/* v2: on a phone the toast lives at the TOP, under the header. At the bottom it
   sat on the very field the message was about (and on «حفظ»). */
@media(max-width:899px){
  .toast-host{bottom:auto;top:var(--toast-top)}
  .toast{animation-name:toast-down}
}
@keyframes toast-down{from{opacity:0;transform:translateY(-14px) scale(.96)}to{opacity:1;transform:none}}
.toast{background:var(--ink);color:var(--canvas);border-radius:var(--r-xl);
  padding:var(--s-md) var(--s-xl);font-size:14px;font-weight:600;max-width:520px;
  box-shadow:var(--depth-2);cursor:pointer;
  animation:pop var(--d-enter) var(--ease-spring)}
@keyframes pop{from{opacity:0;transform:translateY(10px)}to{opacity:1;transform:none}}
.toast.ok{background:var(--primary);color:var(--on-primary)}
.toast.ok::before{content:"✓";font-weight:var(--w-hero);margin-inline-end:var(--s-sm)}
.toast.bad{background:var(--negative-darkest);color:var(--canvas)}
.pager{display:flex;align-items:center;justify-content:center;gap:var(--s-sm);flex-wrap:wrap}
/* صف «حقل + زر» على الهاتف: الحقل سطراً كاملاً والزرّ تحته بارتفاع اللمس.
   nowrap يعني على 390 بكسل حقلاً بعرض ثمانين بكسل — عريضاً بما يكفي ليبدو
   قابلاً للكتابة، وضيّقاً بما يمنع قراءة ما كُتب فيه. */
@media(max-width:899px){
  .row.wrap-m{flex-wrap:wrap}
  .row.wrap-m > .inp{flex:1 1 100%}
  .row.wrap-m > .btn{flex:1 1 100%;min-height:var(--touch)}
}
/* صف مؤشرات على الجوال: شريط أفقي لا أربع بطاقات فوق بعضها.
   أربع بطاقات كاملة تعني تمرير ثلاث شاشات قبل رؤية أول طلب — والمؤشرات
   ملخّص يُلمح إليه، لا شاشة تُقرأ. */
/* 599 was a number this sheet invented; the app switches at 899, so a KPI row
   on a 700px phone got four full cards nobody scrolled past. */
@media(max-width:899px){
  .grid.kpi-strip{
    display:flex; gap:var(--s-sm); overflow-x:auto; scroll-snap-type:x mandatory;
    scrollbar-width:none; padding-block-end:var(--s-xs);
  }
  /* النزف إلى حافة الشاشة اختياري، وكان مفروضاً.
     margin-inline سالبة بمقدار --pad صحيحةٌ فقط إن كان الأب محشوّاً بـ--pad
     بالضبط؛ وفي شاشة الجرد أبوه <div> عارٍ، فيتدلّى الشريط خارج الهامش
     ويُقرأ كأنه بلا حشوة. الافتراضي الآن أن يحترم هامش أبيه أياً كان، ومن
     أراد النزف طلبه. */
  .grid.kpi-strip.bleed{
    margin-inline:calc(var(--pad) * -1); padding-inline:var(--pad);
  }
  .grid.kpi-strip::-webkit-scrollbar{ display:none }
  .grid.kpi-strip > *{ flex:0 0 auto; min-width:11rem; scroll-snap-align:start }
  .grid.kpi-strip .kpi .val{ font-size:var(--t-h2); line-height:var(--t-h2-lh) }
}

/* قائمة بطاقات. كانت تُستعمل في خمسة مواضع بلا قاعدة، فتتلاصق البطاقات
   بلا فاصل — وهو خطأ لا يظهر في أي اختبار لأن jsdom لا يحسب تخطيطاً. */
/* قائمة البطاقات لا تُزحلق الصفحة أفقياً.
   نفس علّة النموذج المنبثق ونفس علاجها: بطاقة تتوسّع فتعرض صفّ أزرار أعرض
   من الشاشة، وليس بين الصفّ و<body> شيء يقصّه، فتنزلق الصفحة كلّها.
   clip لا hidden — hidden يجعل الصندوق حاوية تمرير على ذلك المحور،
   و clip يرفض المنفذ أصلاً. والأوراق السفلية تُركَّب على <body> لا هنا،
   فلا يقصّها شيء. */
.cards{display:flex;flex-direction:column;gap:var(--s-sm);
  min-width:0;overflow-x:clip}
.list-card{background:var(--canvas);border-radius:var(--r-lg);padding:var(--s-lg);
  min-width:0;overflow-wrap:anywhere}
/* بطاقة قابلة للضغط: زر حقيقي حتى تعمل لوحة المفاتيح وقارئ الشاشة،
   بمظهر البطاقة لا بمظهر الزر */
.list-card.as-btn{display:block;width:100%;text-align:start;border:0;cursor:pointer;
  font:inherit;color:inherit;transition:box-shadow var(--d-fast) var(--ease)}
.list-card.as-btn:hover{box-shadow:var(--shadow-1)}
/* حركة عُكست: التاريخ يبقى مقروءاً ولا يُحذف، لكنه لم يعد الرصيد الحالي */
.row-void{opacity:.55;text-decoration:line-through;text-decoration-thickness:1px}
.kv{display:flex;justify-content:space-between;align-items:center;gap:var(--s-md);
  padding:var(--s-sm) 0;font-size:14px;min-width:0}
.kv.kv-wrap{flex-wrap:wrap}
#app-host{overflow-x:clip}
/* صفوف .kv القابلة للضغط تحتاج هدف لمس كافٍ على الجوال */
@media(max-width:899px){ button.kv{min-height:44px;padding-block:10px} }
.kv+.kv{border-top:1px solid var(--canvas-soft)}
/* flex:none here meant a long label plus a long value could not fit and had
   to overflow. It may shrink; it just may not be crushed to nothing. */
.kv>:first-child{color:var(--body);flex:0 1 auto;min-width:0;overflow-wrap:anywhere}
.kv>:last-child{text-align:end;min-width:0;overflow-wrap:anywhere}
.acc-h{display:flex;align-items:center;justify-content:space-between;gap:12px;width:100%;
  padding:var(--s-md) 0;min-height:52px;font-weight:var(--w-strong)}
.acc-h .caret{transition:transform .2s var(--ease);color:var(--mute);flex:none}
/* أي شيء يُفتح ويُغلق، لا .acc-h وحدها: بطاقة المخزون على الجوال زرٌّ عادي
   يحمل aria-expanded، وكانت تدير سهمها بنمطٍ سطري يغلب الورقة فيقتل
   الحركة. القاعدة الآن حيث تنتمي — على الحالة المعلَنة. */
[aria-expanded="true"] .caret{transform:rotate(180deg)}
@media(min-width:900px){
  /* Under the table rather than pinned: on a desktop there is room, and a
     fixed bar would cover the pager. */
  .bulkbar{position:static;margin-top:var(--s-lg);border-radius:var(--r-xl)}
}
.bulkbar{position:fixed;inset-inline:0;bottom:calc(var(--tabbar) + var(--sab));z-index:47;
  background:var(--ink);color:var(--canvas);padding:var(--s-md) var(--pad);
  display:flex;align-items:center;gap:var(--s-sm);flex-wrap:wrap}
/* 88: وضعُ الإخراج. الشريطُ يلتصق بأسفل الشاشة على المكتب أيضاً: هو ما يقول
   «كم اخترتَ وكم يزن» وفيه «التالي» — ولا يُترك تحت خمسين صفّاً يُبحث عنه. */
@media(min-width:900px){
  .bulkbar.pick{position:sticky;bottom:var(--s-md);z-index:5;box-shadow:var(--shadow-1)}
}
.bulkbar.pick .t-cap{color:inherit;opacity:.8}
/* المحدَّدُ يُرى من بعيد: إطارٌ وخلفية، لا خانةٌ صغيرةٌ وحدها. */
.list-card.sel{box-shadow:inset 0 0 0 2px var(--ink);background:var(--primary-pale)}
.pick-mark{display:inline-grid;place-items:center;width:22px;height:22px;flex:none;
  border-radius:var(--r-sm);border:2px solid var(--line);font-weight:var(--w-strong);font-size:14px}
.list-card.sel .pick-mark{background:var(--ink);border-color:var(--ink);color:var(--canvas)}
tbody tr.row-sel td{background:var(--primary-pale)}
tbody tr.row-sel td:first-child{box-shadow:inset 3px 0 0 var(--ink)}
tbody tr[aria-selected]:focus-visible{outline:2px solid var(--ink);outline-offset:-2px}

@media(min-width:900px){  }

/* ---------- شاشة القفل ---------- */
.lock{min-height:100dvh;display:flex;align-items:center;justify-content:center;
  padding:var(--s-lg);padding-top:calc(var(--s-lg) + var(--sat))}
.lock-card{background:var(--canvas);border-radius:var(--r-xl);padding:var(--s-2xl) var(--s-xl);
  width:100%;max-width:400px;text-align:center}
@media(min-width:600px){ .lock-card{padding:var(--s-3xl) var(--s-2xl)} }
.lock-mark{width:64px;height:64px;margin:0 auto;border-radius:var(--r-lg);background:var(--primary);
  display:grid;place-items:center;font-size:32px}
.lock .inp{text-align:center;font-size:17px;letter-spacing:.06em}
.lock-msg{min-height:20px;margin-top:10px;font-size:13px;line-height:20px;font-weight:600;color:var(--body)}
.lock-msg.err{color:var(--negative-darkest)}
.lock-msg.warn{color:var(--warning-deep)}
.shake{animation:shake .32s var(--ease)}
@keyframes shake{
  0%,100%{transform:translateX(0)} 20%{transform:translateX(-7px)}
  40%{transform:translateX(7px)}   60%{transform:translateX(-4px)} 80%{transform:translateX(4px)}
}

/* «تغيّر هذا للتوّ» — kit.js pulse(). A ring that swells once and fades,
   never a fill: the figure inside must stay legible for the whole of it.
   Under reduced motion pulse() never adds .pulse-*; it adds .flash-*, a
   still tint that marks the change without moving anything. */
.pulse-cyan{--pulse:var(--accent-cyan)}
.pulse-warn{--pulse:var(--warning)}
.pulse-neg{--pulse:var(--negative)}
.pulse-cyan,.pulse-warn,.pulse-neg{animation:ajw-pulse calc(var(--d-slow) * 3) var(--ease) 1}
@keyframes ajw-pulse{
  0%  {box-shadow:0 0 0 0 color-mix(in srgb, var(--pulse) 60%, transparent)}
  100%{box-shadow:0 0 0 12px color-mix(in srgb, var(--pulse) 0%, transparent)}
}
.flash-cyan{background:var(--accent-cyan-bg)}
.flash-warn{background:color-mix(in srgb, var(--warning) 24%, var(--canvas))}
.flash-neg{background:color-mix(in srgb, var(--negative) 14%, var(--canvas))}

/* A dossier's figure block (report/dossier.js) — the picture fits its card
   and never scrolls the page sideways. */
.dsr-fig{margin:0}
.dsr-fig img{display:block;max-width:100%;height:auto;border-radius:var(--r-md);background:var(--canvas-soft)}
.dsr-fig figcaption{margin-block-start:var(--s-xs)}

/* ---------- v2: icons and the navigation rail ---------- */
.ico{ width:1em; height:1em; fill:none; stroke:currentColor; stroke-width:1.8;
  stroke-linecap:round; stroke-linejoin:round; flex:none }
.ico-glyph{ line-height:1 }
/* the five apps' tab strips are one component now. Below 900px (and for the
   admin app, which has no phone bar) it is still the horizontal pill strip,
   each tab an icon beside its name; from 900px it is a column on the start edge. */
.seg.rail button{ display:inline-flex; align-items:center; gap:6px; position:relative }
.seg.rail .rail-ic{ display:grid; place-items:center }
.seg.rail .rail-ic .ico{ width:18px; height:18px }
.rail-badge{ display:inline-grid; place-items:center; min-width:18px; height:18px; padding-inline:5px;
  margin-inline-start:4px; border-radius:9px; background:var(--negative); color:var(--canvas);
  font-size:11px; line-height:18px; font-weight:var(--w-strong) }
@media(min-width:900px){
  .with-rail{ display:grid; grid-template-columns:var(--rail-w) minmax(0,1fr);
    column-gap:var(--s-lg); align-items:start }
  .with-rail > :not(.seg.rail){ grid-column:2; min-width:0 }
  .with-rail > .seg.rail{ grid-column:1; grid-row:1 / span 40;
    position:sticky; top:calc(var(--topbar) + var(--sat) + var(--s-md));
    display:flex; flex-direction:column; align-items:stretch; gap:4px; width:var(--rail-w);
    padding:6px; border-radius:var(--r-xl); background:var(--grad-surface);
    box-shadow:var(--hi-edge),var(--depth-1);
    max-height:calc(100dvh - var(--topbar) - var(--sat) - 2 * var(--s-md));
    overflow-y:auto; overflow-x:hidden }
  .seg.rail button{ flex-direction:column; justify-content:center; gap:4px; width:100%;
    min-height:64px; padding:8px 2px; border-radius:var(--r-lg); font-size:11px; line-height:14px;
    transition:background var(--d-mid) var(--ease-out), box-shadow var(--d-mid) var(--ease-out) }
  .seg.rail .rail-ic .ico{ width:22px; height:22px }
  .seg.rail .rail-l{ display:-webkit-box; -webkit-line-clamp:2; -webkit-box-orient:vertical;
    overflow:hidden; text-align:center; overflow-wrap:anywhere }
  .seg.rail button:hover{ background:var(--canvas-soft) }
  .seg.rail button.on{ background:var(--primary-pale); color:var(--ink-deep);
    box-shadow:var(--hi-edge),var(--depth-1) }
  .seg.rail .rail-badge{ position:absolute; inset-block-start:4px; inset-inline-end:6px; margin:0 }
}

/* ---------- v2: dashboards ----------
   Tiles are lit surfaces already (.card); the hero is the dark one with a lime
   glow behind its number. From 900px a strip of tiles fills its row whatever its
   length — five tiles used to be four plus one orphan on a row of its own. */
@media(min-width:900px){
  .grid.kpi-strip{ grid-template-columns:repeat(auto-fit,minmax(min(100%,13.5rem),1fr)) }
}
.kpi{ position:relative }
.kpi-ring{ position:absolute; inset-block-end:0; inset-inline-end:0; line-height:0 }
.kpi-tap{ cursor:pointer }
.kpi-tap:active{ transform:scale(.98); box-shadow:var(--hi-edge),var(--depth-press) }
@media(hover:hover) and (pointer:fine){
  .kpi-tap:hover{ transform:translateY(-2px); box-shadow:var(--hi-edge),var(--depth-2) }
}
/* a bar is a tube: an inset track and a lit fill */
.bar{ box-shadow:inset 0 1px 2px color-mix(in srgb, var(--ink) 14%, transparent) }
.bar>i{ position:relative }
.bar>i::after{ content:""; position:absolute; inset:0; border-radius:inherit; pointer-events:none;
  background:linear-gradient(180deg, color-mix(in srgb, var(--canvas) 42%, transparent), transparent 62%) }
/* progress ring (kit.js ringProgress) */
.ring{ display:block; flex:none }
.ring .trk{ stroke:var(--canvas-soft) }
.ring .val{ stroke:var(--ink); stroke-linecap:round;
  transition:stroke-dashoffset var(--d-slow) var(--ease-out) }
.ring.done .val{ stroke:var(--positive) }
.ring text{ font-size:12px; font-weight:var(--w-hero); fill:var(--ink) }
.card-dark .ring .trk{ stroke:color-mix(in srgb, var(--canvas) 18%, transparent) }
.card-dark .ring .val{ stroke:var(--primary) }
.card-dark .ring text{ fill:var(--canvas) }
/* donut arcs swing in once, in turn (kit.js sets .draw only on a mount) */
.donut .seg-arc.draw{ animation:arc-in var(--d-slow) var(--ease-out) both;
  animation-delay:calc(var(--i,0) * 70ms) }
@keyframes arc-in{ from{ opacity:0; transform:scale(.88) rotate(-14deg) } to{ opacity:1; transform:none } }
/* the accountant's phone balance is the whole answer to «كم معي؟» — a hero */
.mbal{ background:radial-gradient(80% 110% at 100% 0%, color-mix(in srgb, var(--primary) 24%, transparent), transparent 70%),
                   var(--grad-ink);
  color:var(--canvas); border-radius:var(--r-xl); padding:var(--s-xl) var(--s-lg);
  box-shadow:var(--hi-edge),var(--depth-2) }
.mbal .mbal-l{ color:var(--primary-neutral) }
.mbal .mbal-v{ color:var(--primary) }
.mbal .mute{ color:var(--primary-neutral) }

/* the order composer's actions: on a phone they are one bar, stuck above the tab bar */
@media(max-width:899px){
  .of-actions{ position:sticky; inset-block-end:calc(var(--tabbar) + var(--sab) + var(--s-sm)); z-index:20;
    flex-wrap:nowrap; gap:var(--s-sm); padding:var(--s-sm); border-radius:var(--r-xl);
    background:var(--glass); -webkit-backdrop-filter:blur(12px); backdrop-filter:blur(12px);
    box-shadow:var(--hi-edge),var(--depth-2) }
  .of-actions > .row{ flex:1; flex-wrap:nowrap; min-width:0 }
  .of-actions .btn{ flex:1; min-width:0; padding-inline:var(--s-sm); white-space:normal; line-height:18px }
}

/* ---------- v2: entry and loading ----------
   .enter is opt-in (motion.js enter/enterView), never global: every screen is
   torn down and rebuilt on each redraw, so an entry animation on every card
   would replay on every tap. It is applied once, when the person NAVIGATES. */
.enter{animation:enter-up var(--d-enter) var(--ease-out) both;
  animation-delay:calc(var(--i,0) * var(--d-stagger))}
@keyframes enter-up{from{opacity:0;transform:translateY(10px)}to{opacity:1;transform:none}}
/* a placeholder with the shape of what is coming — better than a bare spinner */
.skel{display:block;border-radius:var(--r-md);min-height:1em;
  background:linear-gradient(90deg,var(--canvas-soft) 0%,
    color-mix(in srgb, var(--canvas-soft) 55%, var(--canvas)) 50%,var(--canvas-soft) 100%);
  background-size:200% 100%;animation:skel 1.3s linear infinite}
@keyframes skel{from{background-position:200% 0}to{background-position:-200% 0}}

@media(prefers-reduced-motion:reduce){
  *,*::before,*::after{animation-duration:.001ms !important;animation-iteration-count:1 !important;
    transition-duration:.001ms !important;scroll-behavior:auto !important}
}
@media print{
  /* v2: depth is light on a screen; on paper it is mud. Gradients and shadows go,
     and the surfaces that were a gradient say their flat colour again. */
  .card,.card-sage,.card-green,.card-dark,.btn,.chip,.inp,.toast,.mtabs{box-shadow:none !important}
  .card{background:var(--canvas)} .card-dark{background:var(--ink)}
  .btn-p,.mtab-cam{background:var(--primary);margin-block-end:0}
  /* Hide every piece of chrome and control. The list must name the PLATFORM
     shell (.topbar, .seg, .usermenu) — an earlier version still targeted the
     prototype's header.nav, which no longer exists, so the nav printed on
     every page.

     ⚠ This rule had no declaration block for two releases. The selector list
     ended with a trailing comma, so `body` was swallowed into it and the
     whole group took `{background:#fff}` — meaning NOTHING was ever hidden
     when printing, including the fix the comment above claims to have made.
     The test that was supposed to catch it only checked that these selectors
     appear as text somewhere in the sheet. It now parses the block and
     asserts display:none actually lands on .topbar. Keep it that way. */
  .topbar,.seg,.usermenu,.bellmenu,.fab,.bulkbar,.pager,.chips,.filter-bar,
  .noprint,.toast-host,#tips,#modals,.ov,.search,
  header.nav,.m-top,.m-tabs{display:none !important}

  body{background:#fff}
  .card,.card-sage,.card-green,.list-card{padding:0;border-radius:0;background:#fff;box-shadow:none}
  main,.m-main,.wrap{padding:0;max-width:none}
  thead th{background:#eee}
  /* Tables must print whole rather than scroll. */
  .tbl-wrap{overflow:visible} tbody td{white-space:normal}
  .card-dark{background:#fff;color:#000}
  .kpi .val{font-size:24px}
  /* Never split a card or a table row across pages. */
  .card,tr{break-inside:avoid}
  thead{display:table-header-group}
}

/* ---------- work queue ----------
   One row per thing that needs a person, ordered by money at risk. The
   severity stripe is a BORDER rather than a fill so it survives a mono
   printer and so the row stays readable at any width.

   Shared rather than module-private: the storage dashboard's alerts card is
   the same shape wearing different clothes, and unifying them removes a
   pattern rather than adding one. */
.q-list{display:flex;flex-direction:column}
.q-row{display:flex;align-items:center;gap:var(--s-md);
  padding:var(--s-md) 0 var(--s-md) 0;padding-inline-start:var(--s-md);
  border-block-end:1px solid var(--line-soft);
  border-inline-start:3px solid var(--mute);
  min-height:var(--touch);
  transition:opacity var(--d-mid) var(--ease),transform var(--d-mid) var(--ease)}
.q-row:last-child{border-block-end:0}
.q-row.sev-bad{border-inline-start-color:var(--negative)}
.q-row.sev-warn{border-inline-start-color:var(--warning)}
.q-row.sev-ok{border-inline-start-color:var(--positive)}
.q-body{flex:1;min-width:0}
.q-title{font-weight:var(--w-strong);font-size:var(--t-sm);line-height:var(--t-sm-lh)}
.q-note{margin-top:2px}
.q-amt{flex:none;font-weight:var(--w-strong);text-align:end}
.q-spacer{flex:none;width:0}
/* Resolved: fade, then collapse the height so the rest re-rank visibly.
   The global reduced-motion rule flattens both to nothing, and the JS drops
   the row immediately in that case rather than waiting on a transition that
   will not fire. */
.q-row.gone{opacity:0;transform:scale(.98)}

/* ---------- safes master–detail ---------- */
.safes-split{display:grid;grid-template-columns:1fr;gap:var(--s-lg)}
.safes-list{padding:var(--s-sm);max-height:none;overflow:auto}
.safe-row{display:flex;align-items:center;justify-content:space-between;gap:var(--s-sm);
  width:100%;text-align:start;padding:var(--s-md) var(--s-lg);min-height:var(--touch);
  border-radius:var(--r-md);background:transparent;border:0;cursor:pointer;
  font:inherit;color:inherit;
  transition:background var(--d-fast) var(--ease)}
.safe-row:hover{background:var(--canvas-soft)}
.safe-row.on{background:var(--primary-pale);font-weight:var(--w-strong)}
@media(min-width:900px){
  .safes-split{grid-template-columns:300px 1fr;align-items:start}
  .safes-list{position:sticky;inset-block-start:calc(var(--topbar) + var(--s-lg));
    max-height:calc(100vh - var(--topbar) - var(--s-2xl))}
}

/* ---------- autocomplete ----------
   Shared: sales will want customers, storage wants suppliers, admin wants
   users. Roles, aria-activedescendant and the 44px item height belong in one
   place rather than in each. */
.ac{position:relative}
.ac-list{position:absolute;inset-inline:0;inset-block-start:100%;z-index:55;
  background:var(--canvas);border-radius:var(--r-md);box-shadow:var(--shadow-2);
  max-height:260px;overflow-y:auto;padding:var(--s-xs)}
.ac-item{display:flex;flex-direction:column;align-items:flex-start;gap:2px;
  width:100%;text-align:start;padding:var(--s-sm) var(--s-md);min-height:44px;
  border:0;background:transparent;border-radius:var(--r-sm);cursor:pointer;
  font:inherit;color:inherit;transition:background var(--d-fast) var(--ease)}
.ac-item:hover,.ac-item.on{background:var(--primary-pale)}
.ac-item .badge{align-self:flex-start}
.ac-new{color:var(--ink-deep);font-weight:var(--w-strong)}

/* ---------- the document composer ---------- */
.kind-grid{display:grid;grid-template-columns:1fr;gap:var(--s-sm)}
@media(min-width:600px){ .kind-grid{grid-template-columns:1fr 1fr} }
.kind-tile{display:grid;grid-template-columns:auto 1fr;gap:var(--s-xs) var(--s-md);
  align-items:center;text-align:start;padding:var(--s-md) var(--s-lg);
  min-height:var(--touch);border:0;background:var(--canvas-soft);
  border-radius:var(--r-lg);cursor:pointer;font:inherit;color:inherit;
  transition:background var(--d-fast) var(--ease),transform var(--d-fast) var(--ease)}
.kind-tile:hover{background:var(--primary-pale);transform:translateY(-1px)}
.kind-ic{grid-row:span 2;font-size:20px;line-height:1}
.kind-name{font-weight:var(--w-strong)}
.kind-hint{grid-column:2}

.doc-form{display:flex;flex-direction:column;gap:var(--s-lg)}
.doc-head{display:grid;grid-template-columns:1fr;gap:var(--s-md);align-items:end}
.doc-cash{display:grid;grid-template-columns:1fr;gap:var(--s-md)}
@media(min-width:700px){
  .doc-head{grid-template-columns:1fr 1fr 1fr}
  .doc-cash{grid-template-columns:1fr 1fr}
}
.doc-form label.f{margin-bottom:0}

/* One line. On a phone it stacks; on a desktop it is a row you can Tab
   along, which is what makes a twelve-line invoice feel like typing. */
.doc-line{display:grid;grid-template-columns:1fr;gap:var(--s-sm);
  padding:var(--s-md);border-radius:var(--r-md);background:var(--canvas-soft);
  margin-block-end:var(--s-sm);position:relative}
.doc-line.one{background:transparent;padding:0}
.line-what{display:flex;flex-direction:column;gap:var(--s-xs)}
.line-x{position:absolute;inset-block-start:var(--s-xs);inset-inline-end:var(--s-xs);
  width:var(--touch);height:var(--touch);min-height:var(--touch)}
@media(min-width:700px){
  .doc-line{grid-template-columns:2.2fr .7fr .9fr 1fr 1.1fr auto;align-items:end;
    background:transparent;padding:var(--s-sm) 0;border-radius:0;
    border-block-end:1px solid var(--line-soft)}
  .line-x{position:static}
}
/* The shrink is gated on a POINTER, not on a width. The comment here used to
   say «above 900px, where there is a pointer» while the query said 700px —
   so an iPad in portrait got a 34px delete button on every invoice line.
   Width was never the question; the input device was. */
@media(min-width:700px) and (hover: hover){
  .line-x{width:34px;height:34px;min-height:34px}
}
.line-net{font-weight:var(--w-strong)}

.doc-totals{display:flex;flex-direction:column;gap:var(--s-xs);
  padding:var(--s-md) var(--s-lg);background:var(--canvas-soft);
  border-radius:var(--r-md)}
.doc-totals .kv.total{font-weight:var(--w-hero);font-size:var(--t-lg);
  border-block-start:1px solid var(--line);padding-block-start:var(--s-sm);margin-block-start:2px}

/* The preview keeps the last good answer while a new one is computed. A
   spinner replacing content somebody is reading is worse than stale content
   that says it is stale. */
.doc-preview .tbl-wrap{transition:opacity var(--d-fast) var(--ease)}
.doc-preview .tbl-wrap.stale{opacity:.45}
.doc-preview tfoot td{font-weight:var(--w-strong);
  border-block-start:2px solid var(--ink)}

.doc-issues{display:flex;flex-direction:column;gap:var(--s-sm)}
.issues-line{align-self:flex-start;border:0;background:transparent;cursor:pointer;
  font:inherit;font-weight:var(--w-strong);padding:var(--s-xs) 0;min-height:var(--touch)}
.issues-line.bad{color:var(--negative-darkest)}
.issues-line.warn{color:var(--warning-deep)}
.issues-list{margin:0;padding-inline-start:var(--s-lg);color:var(--body)}
.doc-reversed td{text-decoration:line-through;color:var(--mute)}

/* ---------- reports ---------- */
.rpt-grid{display:grid;grid-template-columns:1fr;gap:var(--s-sm)}
@media(min-width:600px){ .rpt-grid{grid-template-columns:1fr 1fr} }
@media(min-width:1024px){ .rpt-grid{grid-template-columns:1fr 1fr 1fr} }
.rpt-tile{display:flex;flex-direction:column;gap:2px;text-align:start;
  padding:var(--s-md) var(--s-lg);min-height:var(--touch);border:0;
  background:var(--canvas-soft);border-radius:var(--r-lg);cursor:pointer;
  font:inherit;color:inherit;
  transition:background var(--d-fast) var(--ease),transform var(--d-fast) var(--ease)}
.rpt-tile:hover{background:var(--primary-pale);transform:translateY(-1px)}
.rpt-tile-name{font-weight:var(--w-strong)}

.rpt-params{display:grid;grid-template-columns:1fr;gap:var(--s-md)}
@media(min-width:700px){ .rpt-params{grid-template-columns:repeat(4,1fr)} }
.rpt-params label.f{margin-bottom:0}

.rpt-table th.num-h,.rpt-table td.num-c{text-align:end}
.rpt-table td.b,.rpt-table th.b{font-weight:var(--w-strong)}
.rpt-group-head td{background:var(--canvas-soft);font-weight:var(--w-strong);
  padding-block:var(--s-sm)}
.rpt-subtotal td{border-block-start:1px solid var(--line);font-weight:var(--w-strong)}
.rpt-total td{border-block-start:2px solid var(--ink);font-weight:var(--w-hero);
  background:var(--canvas-soft);
  background:var(--canvas-soft)}
/* Tone marks the CELL, not the row: it is one figure that is out of line,
   and colouring the whole row hides which. */
.rpt-table thead th{background:var(--canvas-soft);border-block-end:2px solid var(--ink);
  color:var(--ink);letter-spacing:.02em}
.rpt-table tbody tr:nth-child(even) td{background:var(--canvas-soft)}
.rpt-table td.tone-warn{color:var(--warning-deep)}
.rpt-table td.tone-bad{color:var(--negative-darkest)}
.rpt-table tr.rpt-flag td:first-child{
  border-inline-start:3px solid var(--warning);padding-inline-start:var(--s-sm)}
.rpt-link{border:0;background:transparent;font:inherit;color:var(--ink-deep);
  text-decoration:underline;text-underline-offset:2px;cursor:pointer;
  padding:0;min-height:var(--touch);text-align:start}
@media(min-width:900px){ .rpt-link{min-height:0} }
.rpt-wide{max-height:70vh}

/* ---------- alert accordion ----------
   The severity stripe was an inline style in the prototype; as a class it
   stays consistent across the modules that will reuse this pattern. */
.acc-h{display:flex;align-items:center;justify-content:space-between;gap:var(--s-sm);
  width:100%;text-align:start;padding:var(--s-md) 0;min-height:var(--touch)}
.acc-h.sev-bad{border-inline-start:3px solid var(--negative);padding-inline-start:var(--s-md)}
.acc-h.sev-warn{border-inline-start:3px solid var(--warning);padding-inline-start:var(--s-md)}
.caret{transition:transform .18s var(--ease);display:inline-block}
@media(prefers-reduced-motion:reduce){ .caret{transition:none} }

/* ---- الإقرار الضريبي: صفوف الخانات ---- */
/* A box is a heading with its emirate rows underneath. Weight, not colour:
   the seven rows must read as belonging to box 1 on a mono printout too. */
tbody tr.row-group td{font-weight:var(--w-strong);
  background:color-mix(in oklab,var(--canvas-soft) 60%,transparent)}
tbody td.sub-row{padding-inline-start:var(--s-xl);color:var(--body)}
.warnbox.bad{background:var(--negative);color:var(--canvas)}

/* ============================================================
   نسخة الجوال — أربع مهام
   ============================================================
   Below 899px the accounting module renders a different tree entirely, so
   these classes never collide with the desktop's. Every rule here obeys one
   constraint: a thumb reaches the BOTTOM of a phone comfortably and the top
   corners barely at all, so the balance reads at the top and every action
   that commits something lives in the bottom third.
   ============================================================ */

.mbody{ display:flex; flex-direction:column; gap:var(--s-lg);
        padding-block-end:calc(var(--tabbar) + var(--sab) + var(--s-lg)) }

/* ---- the balance, as the whole answer to «كم معي؟» ---- */
.mbal{ text-align:center; padding-block:var(--s-xl) var(--s-lg) }
.mbal-l{ font-size:var(--t-sm); color:var(--body); font-weight:var(--w-strong) }
.mbal-v{ font-size:clamp(34px,12vw,52px); line-height:1.05;
         font-weight:var(--w-hero); letter-spacing:-.03em; margin-block-start:4px }

/* ---- the tab bar: four items, thumb height, safe-area aware ---- */
/* min-height and --tabbar-h are the same number on purpose: the bar's real
   height and the space everything else reserves for it must not drift, or
   the "is the bar tall enough" check passes by coincidence. */
.mtabs{
  position:fixed; inset-inline:0; inset-block-end:0; z-index:40;
  min-height:var(--tabbar-h);
  display:grid; grid-auto-flow:column; grid-auto-columns:1fr;
  background:var(--glass); border-block-start:1px solid var(--canvas-soft);
  -webkit-backdrop-filter:blur(14px) saturate(1.4); backdrop-filter:blur(14px) saturate(1.4);
  box-shadow:0 -6px 20px color-mix(in srgb, var(--ink) 6%, transparent);
  padding-block-end:env(safe-area-inset-bottom, 0px);
}
.mtab{
  position:relative; display:flex; flex-direction:column; align-items:center;
  justify-content:center; gap:2px; min-height:60px; background:none; border:0;
  color:var(--body); font-weight:var(--w-strong); font-size:11px;
  transition:color var(--d-fast) var(--ease);
}
.mtab.on{ color:var(--ink-deep) }
/* The lit slot: a lime line on the bar's top edge that SLIDES to the slot you
   opened (transform only). --n slots, --k the lit one; the bar is rebuilt on
   every draw so mobile.js seeds --k with where it was and moves it a frame later.
   In RTL slot 0 is on the right, so the travel is mirrored (--dir). */
.mtab-ind{ position:absolute; inset-block-start:0; inset-inline-start:0; pointer-events:none;
  width:calc(100% / var(--n,5)); height:3px; display:grid; place-items:center;
  transform:translateX(calc(var(--k,0) * 100% * var(--dir,1)));
  transition:transform var(--d-enter) var(--ease-spring) }
html[dir="rtl"] .mtab-ind{ --dir:-1 }
.mtab-ind i{ display:block; width:46%; height:3px; background:var(--primary);
  border-radius:0 0 3px 3px; box-shadow:0 0 10px color-mix(in srgb, var(--primary) 70%, transparent) }
/* the icon sits in a pill that fills when its tab is lit */
.mtab-i{ display:grid; place-items:center; width:52px; height:28px; border-radius:var(--r-pill);
  transition:background var(--d-mid) var(--ease-out), transform var(--d-mid) var(--ease-spring) }
.mtab-i .ico{ width:23px; height:23px }
.mtab-i .ico-glyph{ font-size:19px; line-height:1 }
.mtab.on .mtab-i{ background:var(--primary-pale); transform:translateY(-1px) scale(1.06) }

/* الزرّ الدائري المرفوع في منتصف الشريط.
   يخرج فوق حافة الشريط: .mtabs بلا overflow فلا شيء يقصّه، لكنها في الطبقة
   ٤٠ بينما الزرّ العائم في ٤٨ — فيأخذ هذا طبقته حتى لا يمرّ الزرّ العائم
   فوقه حين يلتقيان. */
.mtab-centre{ overflow:visible; z-index:1 }
.mtab-cam{
  display:grid; place-items:center;
  width:var(--fab); height:var(--fab); border-radius:var(--r-pill);
  background:var(--grad-primary); color:var(--on-primary);
  margin-block-start:calc(var(--fab) / -2 - 2px);
  box-shadow:var(--hi-edge),var(--key-edge),var(--depth-2);
  transition:transform var(--d-fast) var(--ease-spring);
}
.mtab-cam .ic{ font-size:24px; line-height:1; display:grid; place-items:center }
.mtab-cam .ico{ width:26px; height:26px; stroke-width:2.2 }
.mtab-centre:active .mtab-cam{ transform:translateY(2px) scale(.96) }
.mtab-centre .mtab-l{ margin-block-start:2px }
.mtab-dot{
  position:absolute; inset-block-start:6px; inset-inline-end:22%;
  min-width:17px; height:17px; padding-inline:4px; border-radius:9px;
  background:var(--negative); color:var(--canvas);
  font-size:10px; line-height:17px; text-align:center;
}

/* ---- primary actions, bottom third, full width ---- */
.mactions{ margin-block-start:auto; display:flex; flex-direction:column;
           gap:var(--s-sm) }
.mbig{ width:100%; min-height:56px; font-size:var(--t-md) }

/* ---- press and hold ----
   Money leaving a safe is the one action here where an accidental tap is
   expensive and undoing it needs a second person. The fill is the feedback;
   the 600ms is the point. */
.hold{ position:relative; overflow:hidden; isolation:isolate }
.hold-fill{
  position:absolute; inset-block:0; inset-inline-start:0; width:0;
  background:var(--ink-deep); opacity:.22; z-index:-1;
}
.hold.holding{ transform:scale(.995) }
.hold-l{ position:relative }

/* ---- the keypad ----
   Ten targets you can hit without looking, because the other hand is
   holding cash. The OS keyboard hides digits behind a shift. */
.mkeypad{ display:grid; grid-template-columns:repeat(3,1fr); gap:var(--s-sm) }
.mkey{
  min-height:56px; font-size:22px; font-weight:var(--w-strong);
  font-variant-numeric:tabular-nums; direction:ltr;
  background:var(--canvas-soft); border:0; border-radius:var(--r-md);
  color:var(--ink); transition:transform var(--d-fast) var(--ease);
}
.mkey:active{ transform:scale(.94) }
.mkey-del{ background:transparent }
.mkeypad-v{
  text-align:center; font-size:clamp(30px,10vw,44px); line-height:1.1;
  font-weight:var(--w-hero); letter-spacing:-.03em;
  padding-block:var(--s-sm); direction:ltr;
}

/* ---- who is it going to: faces, not a dropdown ----
   Four to six people, the same ones every day. A <select> on a phone opens
   a modal wheel that hides the very thing being chosen. */
.mpeople{ display:grid; grid-template-columns:repeat(auto-fill,minmax(84px,1fr));
          gap:var(--s-sm) }
.mperson{
  display:flex; flex-direction:column; align-items:center; gap:6px;
  padding:var(--s-sm); min-height:88px; background:none; border:0;
  border-radius:var(--r-md); color:var(--body);
  transition:background var(--d-fast) var(--ease);
}
.mperson.on{ background:var(--primary-pale) }
.mav{
  display:grid; place-items:center; width:48px; height:48px; border-radius:50%;
  background:var(--canvas-soft); color:var(--ink);
  font-weight:var(--w-hero); font-size:var(--t-sm);
}
.mperson.on .mav{ background:var(--primary); color:var(--on-primary) }
.mname{ font-size:11px; line-height:14px; text-align:center;
        overflow-wrap:anywhere }

/* ---- search, requests, capture ---- */
.msearch input{ min-height:52px; font-size:16px }   /* 16px: iOS will not zoom */
.mrq{ display:flex; gap:var(--s-sm); flex:none }
.mrq .btn{ min-height:44px }

.mshot{ display:flex; gap:var(--s-sm); overflow-x:auto; min-height:96px }
.mshot-i{ height:96px; border-radius:var(--r-md); object-fit:cover }
.mshot-empty{
  display:grid; place-items:center; width:100%; min-height:96px;
  border:1px dashed var(--canvas-soft); border-radius:var(--r-md);
  color:var(--mute); font-size:var(--t-sm);
}

/* A tappable queue row on the phone needs the whole row to be the target. */
.q-tap{ width:100%; text-align:start; border:0; background:none; cursor:pointer }
@media(max-width:899px){ .q-row{ min-height:56px } }

/* رقم النشرة في قائمة الحساب. صغيرٌ وباهت — يُقرأ عند الحاجة ولا يزاحم. */
.usermenu-build{
  padding: var(--s-xs) var(--s-md) var(--s-sm);
  border-top: 1px solid var(--line);
  text-align: left;
}

/* ============================================================
   الإدارة — محرّر صلاحيات الشخص ومحرّر الدور (76)
   ============================================================
   One card per system; rows of [من الدور · مسموح · ممنوع]. On a phone the
   control takes the full width with 44px targets, and only one system card
   is open at a time — the tree is 89 keys long. */
.acc-banner{background:var(--canvas-soft);border-radius:var(--r-md);padding:var(--s-sm) var(--s-md)}
.acc-roles .chip{min-height:var(--touch)}
.acc-roles .chip[disabled]{opacity:.5;cursor:not-allowed}
.acc-sys{background:var(--canvas);border-radius:var(--r-xl);padding:var(--s-md) var(--s-lg);margin-block-end:var(--s-md)}
.acc-sys-h{display:flex;align-items:center;gap:var(--s-sm);width:100%;min-height:var(--touch);text-align:start}
.acc-group{border-block-start:1px solid var(--line);padding-block:var(--s-sm)}
.acc-group-h{display:flex;flex-wrap:wrap;align-items:center;justify-content:space-between;gap:var(--s-sm);min-height:var(--touch)}
.acc-row{display:grid;grid-template-columns:minmax(0,1fr) auto auto;align-items:center;gap:var(--s-sm) var(--s-md);padding-block:var(--s-sm);padding-inline-start:var(--s-sm)}
.acc-row.changed{border-inline-start:3px solid var(--warning-deep);padding-inline-start:calc(var(--s-sm) - 3px)}
.acc-name{min-width:0}
.acc-eff{min-width:var(--touch);justify-content:center}
.tri{display:inline-grid;grid-template-columns:repeat(3,auto);gap:var(--s-xxs);padding:var(--s-xxs);
  background:var(--canvas-soft);border-radius:var(--r-pill)}
.tri button{min-height:var(--touch);padding:0 var(--s-md);border-radius:var(--r-pill);
  font-size:13px;font-weight:var(--w-strong);color:var(--body);white-space:nowrap}
.tri button.on{background:var(--canvas);color:var(--ink);box-shadow:inset 0 0 0 1px var(--ink)}
.tri button.allow.on{background:var(--primary-pale);color:var(--positive-deep);box-shadow:inset 0 0 0 1px var(--positive)}
.tri button.deny.on{background:var(--negative);color:var(--canvas);box-shadow:none}
.tri button[disabled]{opacity:.45;cursor:not-allowed}
.acc-adv summary{min-height:var(--touch);display:flex;align-items:center;cursor:pointer;
  color:var(--mute);font-weight:var(--w-strong);font-size:13px}
.acc-check{align-items:flex-start;padding-block:var(--s-xs)}
.acc-diff-line .badge{white-space:normal;text-align:start}
.acc-save{position:sticky;bottom:0;z-index:5;display:flex;flex-wrap:wrap;align-items:center;gap:var(--s-sm);
  margin-block-start:var(--s-lg);padding:var(--s-md) var(--s-lg);
  padding-block-end:calc(var(--s-md) + var(--sab));
  background:var(--canvas);border-radius:var(--r-xl) var(--r-xl) 0 0;box-shadow:0 -1px 0 var(--line)}
.acc-save .btn{flex:1 1 auto}
.role-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(min(100%,280px),1fr));gap:var(--s-md)}
.audit-diff{gap:var(--s-xxs)}
@media (max-width:899px){
  .acc-row{grid-template-columns:minmax(0,1fr) auto}
  .acc-row .tri{grid-column:1 / -1;grid-template-columns:repeat(3,1fr);width:100%}
  .tri button{padding:0 var(--s-xs)}
}

/* ---------------------------------------------------------- 79: the hall
   The stage track (receive → … → ready), the one big next button, the
   basket's stepper and the order's «مسار الطلب». Tokens only. */
.stage-track{ display:flex; flex-wrap:wrap; gap:var(--s-xs); margin-top:var(--s-sm); }
.stage-track .st{ padding:4px 10px; border-radius:var(--r-pill); background:var(--canvas-soft);
  color:var(--mute); font-size:var(--t-sm); line-height:1.6; }
.stage-track .st.done{ background:var(--primary-pale); color:var(--ink); }
.stage-track .st.now{ background:var(--primary); color:var(--on-primary); font-weight:var(--w-strong); }
.stage-track .st.skip{ text-decoration:line-through; color:var(--mute); }
/* 100: مدير الإنتاج ينقل الدفعة بنقرةٍ على أيّ خطوة — خطّ منقَّط يقول إنّها تُنقر. */
.stage-track .st.ov{ cursor:pointer; box-shadow:inset 0 0 0 1px var(--mute); }
.stage-track .st.ov:focus-visible{ outline:2px solid var(--primary); outline-offset:2px; }
.ov-list{ margin:0; padding-inline-start:var(--s-lg); }
.ov-list li{ margin-block:var(--s-xs); }
.stage-next{ min-height:56px; font-weight:var(--w-strong); flex:1 1 auto; }
.stepper{ display:inline-flex; align-items:center; gap:var(--s-xs); flex:none; }
.stepper .num{ min-width:3.5em; text-align:center; }
.flow .flow-step{ padding:4px 10px; border-radius:var(--r-pill); background:var(--canvas-soft);
  color:var(--mute); font-size:var(--t-sm); }
.flow .flow-step.done{ background:var(--primary-pale); color:var(--ink); }
.flow .flow-step.now{ background:var(--primary); color:var(--on-primary); font-weight:var(--w-strong); }
@media (max-width: 899px){
  .stage-track .st, .flow .flow-step{ min-height:44px; display:inline-flex; align-items:center; }
  .stepper .btn{ min-height:44px; min-width:44px; }
}

/* ---------------------------------------------------------- 95: السجلّ
   packages/ui/history.js — the feed reuses .q-list/.q-row and .audit-diff;
   the journey is a numbered line down the page. Tokens only. */
.hist-filters{ display:grid; grid-template-columns:repeat(auto-fill,minmax(min(100%,160px),1fr));
  gap:var(--s-sm); align-items:end; }
.hist-tools{ justify-content:flex-end; gap:var(--s-sm); }
.hist-banner{ padding:var(--s-sm) var(--s-md); border-radius:var(--r-md);
  background:var(--accent-orange-bg); color:var(--accent-orange-ink); font-size:var(--t-sm); }
.hist-row{ align-items:flex-start; }
.hist-row.hist-child{ border-inline-start-width:1px; padding-block:var(--s-sm); }
.hist-top{ gap:var(--s-sm); align-items:flex-start; }
.hist-meta{ margin-top:var(--s-xxs); }
.hist-actor{ color:var(--ink); font-weight:var(--w-strong); }
.hist-actions{ gap:var(--s-xs); margin-top:var(--s-xs); flex-wrap:wrap; }
.hist-detail{ margin-top:var(--s-xs); padding:var(--s-sm) var(--s-md); border-radius:var(--r-md);
  background:var(--canvas-soft); }
.hist-chg{ font-size:var(--t-sm); line-height:var(--t-sm-lh); }
.hist-old{ color:var(--mute); text-decoration:line-through; }
.hist-new{ color:var(--ink); font-weight:var(--w-strong); }
.hist-kids{ margin-top:var(--s-xs); padding-inline-start:var(--s-md); border-inline-start:2px solid var(--line); }
.hist-foot{ margin-top:var(--s-md); }
.hist-steps{ list-style:none; margin:0; padding:0; display:flex; flex-direction:column; }
.hist-step{ position:relative; padding:var(--s-sm) 0 var(--s-md); padding-inline-start:var(--s-xl);
  border-inline-start:2px solid var(--line); margin-inline-start:var(--s-sm); }
.hist-step::before{ content:""; position:absolute; inset-inline-start:-7px; top:var(--s-md);
  width:12px; height:12px; border-radius:var(--r-full); background:var(--canvas);
  border:2px solid var(--mute); }
.hist-step.st-receipt::before, .hist-step.st-return::before{ border-color:var(--positive); }
.hist-step.st-exit::before, .hist-step.st-write_off::before{ border-color:var(--warning-deep); }
.hist-step.st-ship::before{ border-color:var(--accent-cyan-ink); background:var(--accent-cyan-bg); }
.hist-step.is-del::before{ border-color:var(--negative); }
.hist-step:last-child{ border-inline-start-color:transparent; }
.hist-sum{ margin-top:var(--s-xxs); }
.hist-bal{ white-space:nowrap; }


/* ==== packages/ui/shell.css ==== */
/* ============================================================
   packages/ui/shell.css — الغلاف: الدخول، الشريط العلوي، مشغّل الأنظمة
   ============================================================
   Loaded AFTER app.css, which is the design system ported from the storage
   prototype (tokens from DESIGN-wise.md plus the whole component vocabulary:
   .card .btn .chip .inp .kpi .empty .modal .tbl-wrap .toast …).

   This file therefore adds ONLY what the platform shell introduces and does
   not redefine a single class app.css already owns. An earlier draft declared
   its own .btn/.chip/.kpi/.in, which quietly overrode the established
   components for every ported view — the platform must extend that system,
   not compete with it.
   ============================================================ */


/* ---------------------------------------------------- auth screens */

.auth-wrap{
  min-height:100dvh; display:grid; place-items:center;
  padding:var(--s-lg); padding-block-start:calc(var(--s-lg) + var(--sat));
  background:var(--canvas-soft);
}
.auth-card{
  width:100%; max-width:380px; background:var(--canvas);
  border-radius:var(--r-xl); padding:var(--s-2xl) var(--s-xl);
  box-shadow:var(--shadow-1), var(--shadow-3);
  display:flex; flex-direction:column; gap:var(--s-md);
}
.auth-brand{
  align-self:center; background:var(--primary); color:var(--on-primary);
  font-weight:900; font-size:var(--t-lg); letter-spacing:.5px;
  padding:var(--s-sm) var(--s-lg); border-radius:var(--r-pill);
  margin-block-end:var(--s-sm);
}
.auth-lock{ align-self:center; font-size:34px; line-height:1; }
.auth-title{ margin:0; font-size:var(--t-h2); line-height:30px; font-weight:var(--w-hero); text-align:center; color:var(--ink-deep); }
.auth-sub{ margin:0; text-align:center; color:var(--mute); font-size:var(--t-sm); }
.auth-who{
  text-align:center; font-size:var(--t-sm); color:var(--body);
  background:var(--canvas-soft); border-radius:var(--r-pill);
  padding:var(--s-xs) var(--s-md); align-self:center;
  unicode-bidi:isolate; direction:ltr;
}
.auth-foot{ margin:0; text-align:center; font-size:var(--t-cap); color:var(--mute); }

/* Email and password are Latin even in an RTL page. */
.inp[type="email"], .inp[type="password"]{ direction:ltr; text-align:start; }

.auth-err{ color:var(--negative-darkest); font-size:var(--t-sm); min-height:18px; text-align:center; }
.auth-err:empty{ min-height:0; }

.auth-meter{ font-size:var(--t-cap); color:var(--mute); text-align:end; }
.auth-meter.s0,.auth-meter.s1{ color:var(--negative); }
.auth-meter.s2,.auth-meter.s3{ color:var(--warning-deep); }
.auth-meter.s4,.auth-meter.s5,.auth-meter.s6{ color:var(--positive-deep); }

/* ---------------------------------------------------- top bar */

.topbar{
  position:sticky; top:0; z-index:40;
  background:var(--glass); border-block-end:1px solid var(--canvas-soft);
  -webkit-backdrop-filter:blur(14px) saturate(1.4); backdrop-filter:blur(14px) saturate(1.4);
  box-shadow:0 1px 0 color-mix(in srgb, var(--ink) 4%, transparent), var(--depth-1);
  padding-block-start:var(--sat);
}
/* v2: a slimmer bar on a phone — every pixel of height is a pixel of list. Everything
   that measures against it (--toast-top, the rail's sticky offset, .m-main) reads the token. */
@media (max-width:899px){ :root{ --topbar:52px; } }
.topbar-in{
  height:var(--topbar); display:flex; align-items:center; gap:var(--s-sm);
  padding-inline:var(--pad); margin-inline:auto;
}
.topbar-title{ font-weight:var(--w-hero); color:var(--ink-deep); min-width:0;
               overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }

/* Scrolling down hides the bar and gives the list the full screen; scrolling
   up brings it straight back. Only on small screens — on a desktop the space
   is not scarce and a moving header is just noise. */
@media (max-width:899px){
  .topbar{ transition:transform .22s var(--ease); }
  .topbar.up{ transform:translateY(-100%); }
}
@media (prefers-reduced-motion:reduce){
  .topbar{ transition:none; }
}
.topbar .btn-icon .ico{ width:20px; height:20px }

@media (max-width:640px){
  /* The sync chip's label goes first; the coloured dot still carries it. */
  .chip [data-sync-txt]{ display:none; }
  .topbar-title{ font-size:15px; }
}

/* A <select> styled as one of the existing chips. */
select.chip{
  font:inherit; font-size:var(--t-sm); cursor:pointer; border:0;
  padding:var(--s-xs) var(--s-md); padding-inline-end:var(--s-lg);
  color:var(--body);
}

/* ------------------------------------------- the failed-sync banner */

/* Sits directly under the top bar, in the flow, so it pushes the page down
   rather than covering the first row of it. Sticky like the bar itself:
   scrolling away from an error does not make the error go away. */
.syncbanner{
  position:sticky; inset-block-start:calc(var(--topbar) + var(--sat));
  z-index:39;
  background:var(--canvas-soft);
  border-block-end:1px solid var(--line);
  border-inline-start:4px solid var(--negative);
}
.syncbanner.warn{ border-inline-start-color:var(--warning-deep); }
.syncbanner-in{
  display:flex; align-items:flex-start; gap:var(--s-sm);
  padding:var(--s-md) var(--pad);
  margin-inline:auto;
}
.syncbanner-body{ flex:1; min-width:0; }
.syncbanner-head{
  font-weight:var(--w-hero); font-size:var(--t-sm); color:var(--negative-darkest);
}
.syncbanner.warn .syncbanner-head{ color:var(--warning-deep); }
.syncbanner-hint{ font-size:var(--t-cap); color:var(--body); margin-block-start:2px; }
/* The server's own words, verbatim and selectable — this is the line he
   copies into a message. Latin text in an RTL page, so it is isolated. */
.syncbanner-raw{
  font-size:var(--t-cap); color:var(--mute);
  margin-block-start:var(--s-xs);
  user-select:text; -webkit-user-select:text;
  unicode-bidi:isolate; text-align:start;
  overflow-wrap:anywhere;
}
.syncbanner-x{
  flex:none; background:transparent; border:0; cursor:pointer;
  color:var(--mute); font:inherit; font-size:var(--t-sm);
  min-width:32px; min-height:32px; border-radius:var(--r-sm);
}
.syncbanner-x:hover{ background:var(--canvas); color:var(--ink); }

@media (max-width:899px){
  .syncbanner-in{ flex-wrap:wrap; }
  .syncbanner-body{ flex-basis:100%; }
  .syncbanner-x{ min-width:44px; min-height:44px; }
  /* The bar itself slides away on scroll; the banner must not be left
     hovering over the content where the bar used to be. */
  .syncbanner{ position:static; }
}

/* ---------------------------------------------------- user menu */

.usermenu{ position:relative; }
.usermenu summary{ list-style:none; cursor:pointer; }
.usermenu summary::-webkit-details-marker{ display:none; }

.usermenu-trigger{
  display:flex; align-items:center; gap:var(--s-xs);
  padding:var(--s-xxs) var(--s-sm) var(--s-xxs) var(--s-xxs);
  border-radius:var(--r-pill); min-height:44px;
  transition:background .14s var(--ease);
}
.usermenu-trigger:hover{ background:var(--canvas-soft); }
.usermenu[open] .usermenu-trigger{ background:var(--canvas-soft); }
.usermenu-label{ font-size:var(--t-sm); font-weight:var(--w-strong); color:var(--ink); white-space:nowrap; }
.usermenu-caret{ font-size:var(--t-cap); color:var(--mute); }
.usermenu[open] .usermenu-caret{ transform:rotate(180deg); }
/* On a narrow screen the avatar alone has to carry it, so the caret stays. */
@media (max-width:640px){ .usermenu-label{ display:none; } }

.avatar{
  width:34px; height:34px; border-radius:50%;
  background:var(--ink-deep); color:var(--primary);
  display:grid; place-items:center; font-weight:var(--w-hero); font-size:var(--t-sm);
}
.usermenu-pop{
  position:absolute; inset-block-start:calc(100% + 8px); inset-inline-end:0;
  min-width:220px; background:var(--canvas); border-radius:var(--r-xl);
  box-shadow:var(--shadow-2), var(--shadow-3);
  padding:var(--s-xs); z-index:50; overflow:hidden;
}
.usermenu-head{ padding:var(--s-md); border-block-end:1px solid var(--canvas-soft); }
.usermenu-name{ font-size:var(--t-sm); color:var(--ink); direction:ltr; text-align:start;
                overflow:hidden; text-overflow:ellipsis; }
.usermenu-role{ font-size:var(--t-cap); color:var(--mute); }
.menu-item{
  display:block; width:100%; text-align:start; font:inherit; cursor:pointer;
  padding:var(--s-md); border:none; border-radius:var(--r-sm);
  background:transparent; color:var(--ink); min-height:44px;
}
.menu-item:hover{ background:var(--canvas-soft); }
.menu-item.danger{ color:var(--negative-darkest); }

/* ---------------------------------------------------- the bell */

/* packages/ui/bell.js. A <details> like the account menu beside it, so it
   opens and closes the same way; on a phone the summary opens a bottom sheet
   instead and the popover is never shown. */
.bellmenu{ position:relative; flex:none; }
.bellmenu summary{ list-style:none; cursor:pointer; }
.bellmenu summary::-webkit-details-marker{ display:none; }
.bell{
  position:relative; display:grid; place-items:center;
  width:var(--touch); height:var(--touch); border-radius:var(--r-pill);
  color:var(--ink); transition:background .14s var(--ease);
}
.bell:hover, .bellmenu[open] .bell{ background:var(--canvas-soft); }
.bell-dot{
  position:absolute; inset-block-start:4px; inset-inline-end:2px;
  min-width:18px; height:18px; padding-inline:4px; border-radius:var(--r-pill);
  background:var(--negative); color:var(--canvas);
  font-size:11px; font-weight:var(--w-hero); line-height:18px; text-align:center;
}
.bell-pop{
  position:absolute; inset-block-start:calc(100% + 8px); inset-inline-end:0;
  width:min(360px, calc(100vw - 2 * var(--pad))); max-height:70vh; overflow:auto;
  background:var(--canvas); border-radius:var(--r-xl);
  box-shadow:var(--shadow-2), var(--shadow-3);
  padding:var(--s-xs) var(--s-md) var(--s-md); z-index:50;
}
.bell-head{
  display:flex; align-items:center; justify-content:space-between; gap:var(--s-sm);
  padding-block:var(--s-sm); border-block-end:1px solid var(--canvas-soft);
}
.bell-grp{ padding-block:var(--s-md) var(--s-xxs); }
.bell-row.read .q-title{ font-weight:var(--w-normal); color:var(--body); }
.bell-empty{ padding-block:var(--s-lg); text-align:center; }
.bell-all, .bell-other{
  font:inherit; font-size:var(--t-sm); cursor:pointer; border:0; background:none;
  color:var(--ink); text-decoration:underline; min-height:var(--touch);
  padding-inline:var(--s-xs);
}
.bell-other{ display:block; width:100%; text-align:start; margin-block-start:var(--s-sm); }

/* 390px: home, title, company, sync dot, bell and avatar share one row. The
   company is the one that can be long, so it gives way; the bell keeps its
   full 44px target. */
@media (max-width:640px){
  .topbar-in select.chip, .topbar-in > span.chip:not([data-sync]){
    max-width:30vw; overflow:hidden; text-overflow:ellipsis;
  }
}

/* ---------------------------------------------------- launcher */

.launcher-head{ margin-block:var(--s-xl) var(--s-lg); }
.launcher-title{ margin:0; font-size:clamp(24px,4vw,32px); font-weight:900; color:var(--ink-deep); }
.launcher-sub{ margin:var(--s-xs) 0 0; color:var(--mute); }

.app-grid{
  display:grid; gap:var(--s-lg);
  grid-template-columns:repeat(auto-fill, minmax(220px, 1fr));
}
.app-tile{
  display:block; text-decoration:none; color:inherit;
  background:var(--canvas); border-radius:var(--r-xl);
  padding:var(--s-xl);
  border:1.5px solid transparent;
  box-shadow:var(--shadow-1);
  transition:transform .12s var(--ease), box-shadow .12s var(--ease), border-color .12s;
}
.app-tile:hover{
  transform:translateY(-2px);
  box-shadow:var(--shadow-2);
  border-color:var(--primary);
}
.app-tile:active{ transform:translateY(0); }
.app-icon{
  width:44px; height:44px; border-radius:var(--r-md);
  display:grid; place-items:center; font-size:20px;
  margin-block-end:var(--s-md);
  background:var(--primary-pale); color:var(--ink-deep);
}
.tone-cyan   .app-icon{ background:var(--accent-cyan-bg); color:var(--accent-cyan-ink); }
.tone-orange .app-icon{ background:var(--accent-orange-bg); color:var(--accent-orange-ink); }
.tone-ink    .app-icon{ background:var(--ink-deep); color:var(--primary); }
.app-name{ font-weight:var(--w-hero); font-size:var(--t-h3); color:var(--ink-deep); }
.app-sub{ font-size:var(--t-sm); color:var(--mute); margin-block-start:var(--s-xxs); }

/* ---------------------------------------------------- misc */


.spinner{
  width:28px; height:28px; border-radius:50%; margin-inline:auto;
  border:3px solid var(--primary-pale); border-top-color:var(--ink-deep);
  animation:spin .8s linear infinite;
}
@keyframes spin{ to{ transform:rotate(360deg); } }
@media (prefers-reduced-motion:reduce){
  .spinner{ animation-duration:2s; }
  .app-tile, .app-tile:hover{ transition:none; transform:none; }
}


/* ---------------------------------------------------- touch targets */

@media (max-width:899px){
  /* .btn-sm is 40px, which is fine as a secondary desktop control but under
     the 44px touch guideline. On a phone these ARE the primary controls —
     sort, select, export, and the per-card actions — and Eissa uses them in
     the cold store wearing gloves. */
  .btn-sm{ min-height:44px; }

  /* Same reason, and the same rule (DESIGN-product.md §2.4). .seg is the tab
     strip every module navigates by, so on a phone it is the single most
     tapped control in the system — and it was 40px in storage, admin and
     accounting alike. No test caught it because jsdom has no layout; the
     headless-Chrome pass in tools/browser/check.mjs measures it. */
  .seg button{ min-height:44px; }

  /* The bottom bar, the FAB and the bulk bar are all position:fixed, so
     without this the last card sits underneath them and its actions cannot be
     reached. Each reads --tabbar, which is 0px until an app declares a bar,
     so these three lines are correct with a bar and without one. */
  body.has-mtabs .wrap{ padding-bottom:calc(var(--tabbar) + var(--sab) + var(--s-lg)); }
  body.has-fab   .wrap{ padding-bottom:calc(var(--tabbar) + var(--sab) + 88px); }
  body.has-fab2  .wrap{ padding-bottom:calc(var(--tabbar) + var(--sab) + 144px); }
  body.has-bulk  .wrap{ padding-bottom:calc(var(--tabbar) + var(--sab) + 150px); }
}

/* A planned module: visible so the roadmap is legible, plainly not usable. */
.app-tile.soon{
  opacity:.55; cursor:default; box-shadow:none;
  background:transparent; border:1.5px dashed var(--line);
}
.app-tile.soon:hover{ transform:none; box-shadow:none; border-color:var(--line); }

/* A rejected write, explained. The raw server text is deliberately whole and
   selectable: truncating it to 60 characters is what made three unrelated
   failures look like the same permissions problem. */
.deadrow{ padding-block:var(--s-xs); border-block-start:1px solid var(--line) }
.deadrow:first-child{ border-block-start:0 }
.deadraw{
  margin:0; white-space:pre-wrap; overflow-wrap:anywhere;
  user-select:text; -webkit-user-select:text;
  unicode-bidi:isolate; text-align:start;
  max-height:6em; overflow:auto;
}


/* ==== packages/ui/login.css ==== */
/* ============================================================
   packages/ui/login.css — مشهد الدخول
   ============================================================
   Loaded after shell.css, whose .auth-* rules still dress «النظام غير
   مهيّأ». Everything here is scoped under .auth-split, the layout
   packages/auth/login.js builds for the sign-in and lock screens.

   Tokens only (the hex test reads this file too), weights 400/600/900,
   durations as multiples of --d-*, the one --ease curve. The emblem is
   animated in logo.svg's own coordinates: transform-box:view-box makes a
   transform-origin of "96.5px 72px" mean the crown of the palm.
   ============================================================ */

/* ---------------------------------------------------------- layout */

.auth-split{
  min-height:100dvh; display:grid;
  grid-template-columns:minmax(400px, .9fr) 1.1fr;
  background:var(--canvas);
  --glow:var(--auth-glow-day);
  --px:0; --py:0;
}
.auth-split[data-part="dawn"] { --glow:var(--auth-glow-dawn); }
.auth-split[data-part="dusk"] { --glow:var(--auth-glow-dusk); }
.auth-split[data-part="night"]{ --glow:var(--auth-glow-night); }

.auth-pane{
  position:relative; display:flex; flex-direction:column;
  align-items:center; justify-content:center;
  padding:var(--s-3xl) var(--s-2xl);
  padding-block-start:calc(var(--s-3xl) + var(--sat));
}
.auth-split .auth-card{
  width:100%; max-width:380px; padding:0;
  background:transparent; box-shadow:none; border-radius:0;
  gap:var(--s-md);
}
.auth-legal{
  position:absolute; inset-inline:0; inset-block-end:calc(var(--s-lg) + var(--sab));
  margin:0; text-align:center; font-size:var(--t-cap); color:var(--mute);
}

/* ---------------------------------------------------------- the form pane */

.auth-hello{
  align-self:flex-start; display:inline-flex; align-items:center; gap:var(--s-sm);
  font-size:var(--t-sm); font-weight:var(--w-strong); color:var(--ink-deep);
  background:var(--canvas-soft); border-radius:var(--r-pill);
  padding:var(--s-xs) var(--s-md);
}
.auth-hello::before{
  content:""; width:8px; height:8px; border-radius:50%;
  background:var(--glow);
  box-shadow:0 0 0 3px color-mix(in srgb, var(--glow) 30%, transparent);
}
.auth-split .auth-title{
  text-align:start; font-size:var(--t-h1); line-height:var(--t-h1-lh);
  color:var(--ink-deep); margin-block-start:var(--s-xs);
}
.auth-split .auth-sub{ text-align:start; font-size:var(--t-body); color:var(--body); }
.auth-split .auth-foot{ text-align:start; font-size:var(--t-sm); color:var(--mute); }

.auth-form{ display:flex; flex-direction:column; gap:var(--s-md); margin-block-start:var(--s-sm); }

/* A floating label. The label sits over the empty field and rises when the
   field has focus or a value — :placeholder-shown sees the value, and the
   two autofill selectors see a password manager that never fires "input".
   They are separate rules on purpose: one unknown pseudo-class in a
   selector list drops the whole rule. */
.auth-field{ position:relative; }
.auth-field .auth-inp{
  min-height:56px; padding:24px var(--s-lg) 8px;
  font-size:var(--t-body); line-height:24px;
  direction:ltr; text-align:right;         /* stays LTR when the password is revealed */
}
.auth-field.has-extra .auth-inp{ padding-left:52px; }
.auth-field label{
  position:absolute; inset-inline-start:var(--s-lg); top:50%;
  transform:translateY(-50%); transform-origin:100% 50%;
  font-size:var(--t-body); line-height:24px; color:var(--mute);
  pointer-events:none;
  transition:transform var(--d-mid) var(--ease), color var(--d-fast) var(--ease);
}
.auth-inp:focus + label,
.auth-inp:not(:placeholder-shown) + label{
  transform:translateY(calc(-50% - 13px)) scale(.78); color:var(--body);
}
.auth-inp:-webkit-autofill + label{ transform:translateY(calc(-50% - 13px)) scale(.78); }
.auth-inp:autofill + label{ transform:translateY(calc(-50% - 13px)) scale(.78); }
.auth-inp:-webkit-autofill{
  -webkit-text-fill-color:var(--ink);
  box-shadow:inset 0 0 0 1px var(--ink), inset 0 0 0 40px var(--primary-pale);
}
.auth-inp[aria-invalid="true"]{
  box-shadow:inset 0 0 0 2px var(--negative),
             0 0 0 3px color-mix(in srgb, var(--negative) 14%, transparent);
}

/* Show / hide the password: a real button, 44px, on the field's end. */
.auth-eye{
  position:absolute; inset-inline-end:6px; top:50%; transform:translateY(-50%);
  width:44px; height:44px; display:grid; place-items:center;
  border:0; border-radius:var(--r-sm); background:transparent; color:var(--body);
  cursor:pointer; transition:background var(--d-fast) var(--ease);
}
.auth-eye:hover{ background:var(--canvas-soft); }
.auth-eye svg{ width:22px; height:22px; fill:none; stroke:currentColor;
               stroke-width:1.7; stroke-linecap:round; stroke-linejoin:round; }
/* The icon shows the action: an open eye to reveal, struck through to hide. */
.auth-eye .eye-slash{ stroke-dasharray:24; stroke-dashoffset:24;
                      transition:stroke-dashoffset var(--d-mid) var(--ease); }
.auth-eye[aria-pressed="true"] .eye-slash{ stroke-dashoffset:0; }

.auth-caps{
  align-self:flex-start; font-size:var(--t-cap); font-weight:var(--w-strong);
  color:var(--warning-content);
  background:color-mix(in srgb, var(--warning) 24%, var(--canvas));
  padding:var(--s-xs) var(--s-md); border-radius:var(--r-pill);
}

.auth-split .auth-err{ text-align:start; }
.auth-split .auth-err:not(:empty){
  padding:var(--s-sm) var(--s-md); border-radius:var(--r-md);
  background:color-mix(in srgb, var(--negative) 8%, var(--canvas));
  animation:ajw-rise var(--d-mid) var(--ease);
}
.auth-split .auth-err.is-notice:not(:empty){
  color:var(--warning-content);
  background:color-mix(in srgb, var(--warning) 20%, var(--canvas));
}

/* The submit: busy → a spinner, success → a tick, while the shell loads. */
.auth-form .btn-p{
  position:relative; overflow:hidden; min-height:56px;
  font-size:var(--t-body); margin-block-start:var(--s-xs);
}
.auth-form .btn-p:disabled{ opacity:1; cursor:progress; }
.auth-form .btn-p.is-done{ background:var(--primary-active); }
.auth-spin{
  width:18px; height:18px; border-radius:50%;
  border:2px solid color-mix(in srgb, var(--on-primary) 22%, transparent);
  border-top-color:var(--on-primary);
  animation:spin calc(var(--d-slow) * 2) linear infinite;
}
.auth-tick{
  width:7px; height:13px; margin-block-start:-3px;
  border:solid var(--on-primary); border-width:0 2.5px 2.5px 0;
  transform:rotate(45deg);
  animation:ajw-tick var(--d-mid) var(--ease) both;
}
@media (hover:hover) and (pointer:fine){
  /* A slow sheen across the button — the one flourish on the form. */
  .auth-form .btn-p::after{
    content:""; position:absolute; inset:0; pointer-events:none;
    background:linear-gradient(100deg, transparent 30%,
      color-mix(in srgb, var(--canvas) 45%, transparent) 50%, transparent 70%);
    transform:translateX(120%);
    transition:transform calc(var(--d-slow) * 2) var(--ease);
  }
  .auth-form .btn-p:hover:not(:disabled)::after{ transform:translateX(-120%); }
}

/* The lock screen's padlock and the account it belongs to. */
.auth-split .auth-lock{
  align-self:flex-start; width:56px; height:56px; border-radius:var(--r-lg);
  display:grid; place-items:center; background:var(--ink);
}
.auth-split .auth-lock svg{ width:26px; height:26px; fill:none; stroke:var(--primary);
                            stroke-width:1.8; stroke-linecap:round; }
.auth-split .auth-who{ align-self:flex-start; }

/* Three classes deep so it outranks the entrance stagger on .auth-card > *. */
.auth-split .auth-form.is-shaking{ animation:ajw-shake var(--d-slow) var(--ease); }

/* ---------------------------------------------------------- the ink band */

.auth-hero{
  position:relative; overflow:hidden; isolation:isolate;
  margin:var(--s-md); margin-inline-start:0; border-radius:var(--r-xl);
  background:var(--ink); color:var(--canvas);
  display:flex; flex-direction:column; align-items:center; justify-content:center;
  gap:var(--s-xl); padding:var(--s-3xl) var(--s-2xl) calc(var(--s-3xl) * 2);
}

.auth-lattice{
  position:absolute; inset:-28px; width:calc(100% + 56px); height:calc(100% + 56px);
  z-index:-1; pointer-events:none;
  -webkit-mask-image:radial-gradient(closest-side, black 20%, transparent);
          mask-image:radial-gradient(closest-side, black 20%, transparent);
  transform:translate(calc(var(--px) * -6px), calc(var(--py) * -6px));
  transition:transform var(--d-slow) var(--ease);
}
.auth-lattice path{ fill:none; stroke:var(--auth-line); stroke-width:1; }

.auth-mark{
  position:relative; width:min(34vmin, 300px);
  transform:translate(calc(var(--px) * 10px), calc(var(--py) * 8px));
  transition:transform var(--d-slow) var(--ease);
}
/* The sun behind the palm, tinted by the hour. */
.auth-mark::before{
  content:""; position:absolute; inset:-38%; z-index:-1; border-radius:50%;
  background:radial-gradient(closest-side,
    color-mix(in srgb, var(--glow) 42%, transparent),
    color-mix(in srgb, var(--glow) 10%, transparent) 58%, transparent 74%);
  animation:ajw-breathe calc(var(--d-slow) * 16) var(--ease) infinite alternate;
}
.auth-emblem{ display:block; width:100%; height:auto; overflow:visible; }
.auth-emblem path{ fill:var(--primary); }
.auth-emblem g, .auth-emblem path{ transform-box:view-box; }

/* Entrance, in order: the sun turns in, the trunk rises, the fronds unfurl
   from the crown one by one, the dates settle. About 1.2s end to end, and
   none of it stands between the person and the form. */
.em-sun{ transform-origin:95px 79px;
         animation:ajw-sun calc(var(--d-slow) * 2) var(--ease) both; }
.em-trunk{ transform-origin:87px 101px;
           animation:ajw-grow calc(var(--d-slow) * 1.4) var(--ease) var(--d-mid) both; }
.em-fronds path{
  transform-origin:96.5px 72px;
  animation:ajw-unfurl calc(var(--d-slow) * 1.6) var(--ease)
            calc(var(--d-slow) + var(--i) * var(--d-fast) * .45) both;
}
.em-dates path{
  transform-origin:95px 108px;
  animation:ajw-settle calc(var(--d-slow) * 1.6) var(--ease)
            calc(var(--d-slow) * 1.9 + var(--i) * var(--d-fast) * .6) both;
}
/* Then the crown breathes in a breeze you can barely see. */
.em-fronds{
  transform-origin:96.5px 72px;
  animation:ajw-sway calc(var(--d-slow) * 22) var(--ease) calc(var(--d-slow) * 5) infinite;
}

.auth-words{ text-align:center; }
.auth-wordmark{
  font-size:clamp(36px, 4.2vw, var(--t-hero)); line-height:1.2;
  font-weight:var(--w-hero); color:var(--canvas);
  /* الذهبية: one golden pass of light across the name, then still. */
  background:linear-gradient(100deg, var(--canvas) 42%, var(--glow) 50%, var(--canvas) 58%);
  background-size:260% 100%; background-position:100% 0;
  -webkit-background-clip:text; background-clip:text; -webkit-text-fill-color:transparent;
  animation:ajw-rise calc(var(--d-slow) * 1.6) var(--ease) calc(var(--d-slow) * 1.4) both,
            ajw-shine calc(var(--d-slow) * 5) var(--ease) calc(var(--d-slow) * 3.2) both;
}
.auth-kicker{
  margin-block-start:var(--s-xs); font-size:var(--t-lg); font-weight:var(--w-strong);
  color:var(--primary-neutral);
  animation:ajw-rise calc(var(--d-slow) * 1.6) var(--ease) calc(var(--d-slow) * 1.7) both;
}

/* The four systems, lit in turn — a pallet passing through the business. */
.auth-flow{
  position:absolute; inset-inline:var(--s-2xl); inset-block-end:var(--s-2xl);
  animation:ajw-rise calc(var(--d-slow) * 1.6) var(--ease) calc(var(--d-slow) * 2.4) both;
}
.auth-flow-track{
  position:absolute; inset-inline:var(--s-xl); top:50%; height:1px;
  background:var(--auth-line); overflow:hidden;
}
.auth-flow-track::after{
  content:""; position:absolute; inset-block:0; right:0; width:30%;
  background:linear-gradient(to left, transparent, var(--primary), transparent);
  animation:ajw-travel calc(var(--d-slow) * 19) linear infinite;
}
.auth-flow-steps{
  position:relative; list-style:none; margin:0; padding:0;
  display:flex; justify-content:space-between; gap:var(--s-sm);
}
.auth-flow-steps li{
  font-size:var(--t-cap); font-weight:var(--w-strong); color:var(--mute);
  padding:var(--s-xs) var(--s-md); border-radius:var(--r-pill);
  background:var(--ink); box-shadow:inset 0 0 0 1px var(--auth-line);
  animation:ajw-step calc(var(--d-slow) * 19) linear
            calc(var(--i) * var(--d-slow) * 4.75) infinite;
}

/* Locked: the same scene, holding its breath. */
.is-lock .auth-mark{ opacity:.55; }
.is-lock .em-fronds{ animation:none; }

/* A hidden tab spends nothing. */
.auth-split.is-paused *, .auth-split.is-paused *::before, .auth-split.is-paused *::after{
  animation-play-state:paused !important;
}

/* The form rises in a short stagger; it is usable from the first frame. */
.auth-split .auth-card > *{ animation:ajw-rise calc(var(--d-mid) * 2) var(--ease) both;
                            animation-delay:calc(var(--d-fast) * .5 * var(--n, 0)); }
.auth-split .auth-card > :nth-child(2){ --n:1; }
.auth-split .auth-card > :nth-child(3){ --n:2; }
.auth-split .auth-card > :nth-child(4){ --n:3; }
.auth-split .auth-card > :nth-child(5){ --n:4; }
.auth-split .auth-card > :nth-child(n+6){ --n:5; }

/* ---------------------------------------------------------- into the app */

/* Once, straight after signing in (login.js markEntering): the chrome
   fades up and the launcher's tiles rise into place. */
body.ajw-entering .topbar{ animation:ajw-fade var(--d-slow) var(--ease) both; }
body.ajw-entering .launcher-head,
body.ajw-entering .app-tile{
  animation:ajw-rise calc(var(--d-mid) * 2) var(--ease) both;
  animation-delay:calc(var(--d-fast) * .45 * var(--n, 0) + var(--d-fast));
}
body.ajw-entering .app-grid > :nth-child(2){ --n:1; }
body.ajw-entering .app-grid > :nth-child(3){ --n:2; }
body.ajw-entering .app-grid > :nth-child(4){ --n:3; }
body.ajw-entering .app-grid > :nth-child(n+5){ --n:4; }

/* ---------------------------------------------------------- narrow screens */

@media (max-width:1199px){
  .auth-split{ grid-template-columns:1fr 1fr; }
  .auth-flow-steps li{ padding:var(--s-xs) var(--s-sm); }
}

/* Phones and tablets: the ink band on top, the form as a sheet over it. */
@media (max-width:899px){
  .auth-split{ display:flex; flex-direction:column; background:var(--ink); }
  .auth-hero{
    order:-1; flex:none; margin:0; border-radius:0;
    min-height:clamp(210px, 34dvh, 300px); gap:var(--s-md);
    padding:calc(var(--s-xl) + var(--sat)) var(--s-lg) calc(var(--s-xl) * 2);
  }
  .auth-mark{ width:clamp(84px, 24vw, 120px); }
  .auth-wordmark{ font-size:var(--t-h1); }
  .auth-kicker{ font-size:var(--t-sm); margin-block-start:0; }
  .auth-flow{ display:none; }
  .auth-pane{
    flex:1; position:relative; z-index:1; justify-content:flex-start;
    margin-block-start:calc(-1 * var(--s-xl));
    background:var(--canvas); border-radius:var(--r-xl) var(--r-xl) 0 0;
    padding:var(--s-2xl) var(--s-lg) calc(var(--s-xl) + var(--sab));
    animation:ajw-rise calc(var(--d-slow) * 1.4) var(--ease) both;
  }
  .auth-legal{ position:static; margin-block-start:var(--s-xl); }
}

/* ---------------------------------------------------------- keyframes */

@keyframes ajw-rise{ from{ opacity:0; transform:translateY(10px); } to{ opacity:1; transform:none; } }
@keyframes ajw-fade{ from{ opacity:0; } to{ opacity:1; } }
@keyframes ajw-sun{ from{ opacity:0; transform:rotate(-70deg) scale(.82); } to{ opacity:1; transform:none; } }
@keyframes ajw-grow{ from{ opacity:0; transform:scaleY(.1); } to{ opacity:1; transform:none; } }
@keyframes ajw-unfurl{ from{ opacity:0; transform:rotate(-16deg) scale(.3); } to{ opacity:1; transform:none; } }
@keyframes ajw-settle{ from{ opacity:0; transform:translateY(12px) scale(.9); } to{ opacity:1; transform:none; } }
@keyframes ajw-sway{
  0%, 100%{ transform:rotate(0); }
  30%     { transform:rotate(1.2deg); }
  70%     { transform:rotate(-.9deg); }
}
@keyframes ajw-breathe{ from{ opacity:.7; transform:scale(.96); } to{ opacity:1; transform:scale(1.04); } }
@keyframes ajw-shine{ from{ background-position:100% 0; } to{ background-position:0 0; } }
@keyframes ajw-travel{ from{ transform:translateX(0); } to{ transform:translateX(-340%); } }
@keyframes ajw-step{
  0%, 26%, 100%{ color:var(--mute); box-shadow:inset 0 0 0 1px var(--auth-line); }
  4%, 20%      { color:var(--primary);
                 box-shadow:inset 0 0 0 1px color-mix(in srgb, var(--primary) 55%, transparent); }
}
@keyframes ajw-shake{
  0%, 100%{ transform:none; }
  20%, 60%{ transform:translateX(-8px); }
  40%, 80%{ transform:translateX(8px); }
}
@keyframes ajw-tick{ from{ transform:rotate(45deg) scale(0); } to{ transform:rotate(45deg) scale(1); } }


/* ==== packages/ui/fridge-map.css ==== */
/* ============================================================
   packages/ui/fridge-map.css — خريطة مواقع الثلاجة
   ============================================================
   A seat map, and it is meant to feel like one. Somebody who has picked a
   seat on a phone already knows how to read this: a plan you look down on,
   an aisle down the middle, rows numbered along the edge, colours that mean
   something, and one tap to choose.

   Two builds, one set of data:

     desktop  a 3D model of the cold store. The four floors stand stacked
              behind the deck like storeys of a building; the chosen floor is
              laid out flat in front, tilted just enough that a pallet reads
              as a box rather than a square.

     mobile   the same floor, flat, full width, with the floors as a strip
              across the top. No perspective at all: at 375px a tilted grid
              is a grid nobody can hit.

   Every colour is a token. Every duration is a token. The map is inert under
   prefers-reduced-motion because app.css's block reaches all of it.
   ============================================================ */

/* ------------------------------------------------------------ shell */

.fm{ --fm-gap:4px; --fm-seat:38px; --fm-radius:var(--r-sm); }

.fm-top{
  display:flex; align-items:center; justify-content:space-between;
  gap:var(--s-md); flex-wrap:wrap; margin-block-end:var(--s-md);
}

/* الباب، أسفل الخريطة. Without it "صف ١" and "صف ٩" are just numbers; with it
   they are a direction you can walk in — and the numbering now starts at the
   door, where the counting starts. */
.fm-door,.fm-back{ max-width:660px; margin-inline:auto; }
.fm-door{
  display:flex; align-items:center; justify-content:center; gap:var(--s-sm);
  background:var(--ink); color:var(--primary);
  border-end-start-radius:var(--r-lg); border-end-end-radius:var(--r-lg);
  padding:var(--s-sm) var(--s-md);
  font-size:var(--t-cap); line-height:var(--t-cap-lh); font-weight:var(--w-strong);
  letter-spacing:.04em;
}
.fm-door::before,.fm-door::after{
  content:""; flex:1; height:2px; background:var(--primary); opacity:.35;
  border-radius:var(--r-pill);
}
.fm-back{
  text-align:center; padding:var(--s-xs) var(--s-md);
  background:var(--canvas-soft); color:var(--mute);
  border-start-start-radius:var(--r-lg); border-start-end-radius:var(--r-lg);
  font-size:var(--t-cap); line-height:var(--t-cap-lh); font-weight:var(--w-strong);
}

/* ------------------------------------------------------- the deck */

/* المنظور يميل نحو الباب لا نحو نهاية الثلاجة.
   كانت القيمتان ٣٠٪ و٢٠٪ لأن الباب كان أعلى الصفحة، فالأقرب إلى الناظر هو
   الأعلى. وقد صار الباب أسفلها. */
.fm-stage{ perspective:1600px; perspective-origin:50% 70%; }

.fm-deck{
  transform-style:preserve-3d;
  transition:transform var(--d-slow) var(--ease);
  transform:rotateX(var(--fm-rx,0deg)) scale(var(--fm-z,1));
  transform-origin:50% 80%;
}

.fm-grid{
  display:grid;
  grid-template-columns:auto repeat(7, minmax(0,1fr)) auto;
  gap:var(--fm-gap);
  align-items:center;
  transform-style:preserve-3d;
  /* A seat map is read as a plan of a place, and a plan does not stretch to
     fill a monitor. Unbounded, each seat grew to 132px and stopped reading
     as a seat. */
  max-width:660px;
  margin-inline:auto;
}

/* رقم الصف على الحافتين، كما في خريطة مقاعد الطائرة */
.fm-rowno{
  font-size:var(--t-cap); line-height:var(--t-cap-lh); font-weight:var(--w-strong);
  color:var(--mute); text-align:center; min-width:22px;
  font-variant-numeric:tabular-nums;
}
.fm-rowno.on{ color:var(--ink); }

/* الممر */
.fm-aisle{
  min-height:var(--fm-seat);
  display:flex; align-items:center; justify-content:center;
  font-size:10px; color:var(--mute); letter-spacing:.3em;
}
.fm-aisle::before{
  content:""; width:2px; height:var(--fm-seat);
  background:repeating-linear-gradient(to bottom,
    var(--line) 0 6px, transparent 6px 12px);
}

/* ------------------------------------------------------- the seats */

.fm-slot{
  --c:var(--v-free);
  position:relative;
  min-height:var(--fm-seat);
  border:0; padding:0; margin:0;
  border-radius:var(--fm-radius);
  background:var(--c);
  color:var(--ink);
  font:inherit;
  font-size:10px; line-height:12px; font-weight:var(--w-strong);
  cursor:pointer;
  display:flex; align-items:center; justify-content:center;
  transform-style:preserve-3d;
  transition:transform var(--d-fast) var(--ease),
             box-shadow var(--d-fast) var(--ease),
             opacity var(--d-mid) var(--ease);
  overflow:hidden;
}

/* فارغ: مجرد خانة في انتظار طبلية */
.fm-slot.free{
  background:transparent;
  box-shadow:inset 0 0 0 1px var(--line);
}
.fm-slot.free::after{
  content:"＋"; color:var(--line); font-size:14px; line-height:1;
  opacity:0; transition:opacity var(--d-fast) var(--ease);
}
.fm-slot.free:hover::after,.fm-slot.free:focus-visible::after{ opacity:1; }

/* مشغول: الشريط العلوي هو ما يجعلها تُقرأ كصندوق لا كمربع */
.fm-slot.occ::before{
  content:""; position:absolute; inset-inline:0; inset-block-start:0;
  height:6px;
  background:linear-gradient(to bottom,
    rgba(255,255,255,.55), rgba(255,255,255,0));
  pointer-events:none;
}
.fm-slot.occ{ box-shadow:var(--shadow-1); }

/* الأصناف من الثامن فصاعداً تحمل تظليلاً مائلاً أيضاً: اللون وحده لا يكفي
   لأربعة عشر صنفاً، ولا يكفي أصلاً لمن لا يميز الألوان. */
.fm-slot[data-tex="1"]::after{
  content:""; position:absolute; inset:0; pointer-events:none;
  background:repeating-linear-gradient(45deg,
    rgba(255,255,255,.34) 0 3px, transparent 3px 7px);
}

/* رصيد جزئي: نصف الخانة مملوء، فيُرى الفرق دون قراءة رقم */
.fm-slot.part::after{
  content:""; position:absolute; inset-block-start:0; inset-inline-start:0;
  width:100%; height:46%;
  background:repeating-linear-gradient(-45deg,
    rgba(255,255,255,.42) 0 2px, transparent 2px 6px);
  pointer-events:none;
}

/* معطّل */
.fm-slot.blocked{
  background:var(--v-blocked); color:var(--mute); cursor:not-allowed;
}
.fm-slot.blocked::after{
  content:""; position:absolute; inset:0; pointer-events:none;
  background:repeating-linear-gradient(45deg,
    var(--canvas) 0 2px, transparent 2px 6px);
  opacity:.7;
}

.fm-slot:hover:not(.blocked),
.fm-slot:focus-visible{
  transform:translate3d(0,-2px,14px) scale(1.06);
  box-shadow:var(--shadow-2);
  z-index:3;
}
.fm-slot:focus-visible{ outline:2px solid var(--ink); outline-offset:2px; }

/* المختار — وهذا هو الأخضر الوحيد على الخريطة: الاختيار فعلٌ، وبقية الألوان
   بيانات. */
.fm-slot.sel{
  background:var(--primary); color:var(--on-primary);
  box-shadow:0 0 0 3px var(--ink), var(--shadow-2);
  transform:translate3d(0,-3px,20px) scale(1.1);
  z-index:4;
}
.fm-slot.sel::after{
  content:"✓"; position:absolute; inset:0; display:flex;
  align-items:center; justify-content:center;
  font-size:15px; font-weight:var(--w-hero); background:none;
}

/* عند عزل صنف من مفتاح الألوان */
.fm-slot.dim{ opacity:.16; }

/* في وضع اختيار الموقع: الفارغ ينبض والمشغول يخفت ولا يُضغط */
.fm.placing .fm-slot.occ,
.fm.placing .fm-slot.part{ opacity:.3; cursor:default; }
.fm.placing .fm-slot.free{
  box-shadow:inset 0 0 0 2px var(--primary);
  animation:fm-pulse 2.4s var(--ease) infinite;
}
@keyframes fm-pulse{
  0%,100%{ background:transparent }
  50%{ background:var(--primary-pale) }
}

/* -------------------------------------------- the building model */

/* أربعة أدوار واقفة خلف بعضها، كنموذج مبنى. هذا هو مُنتقي الدور على الشاشات
   الكبيرة، وهو أيضاً ما يجعلك ترى أين المساحة الفارغة دون تبديل الأدوار. */
.fm-tower{
  flex:none; width:236px;
  perspective:1000px; perspective-origin:50% 40%;
  padding-block:var(--s-lg);
}
.fm-tower-inner{ display:grid; gap:var(--s-lg); }

/* The BUTTON stays flat, so its label is ordinary readable text. Only the
   plate inside it is tilted. An earlier version rotated the whole button and
   counter-rotated the label back out of it, which produced text at an angle
   nobody could read at any size. */
.fm-floor{
  position:relative;
  border:0; background:none; padding:0; font:inherit; cursor:pointer;
  display:grid; gap:var(--s-xs); justify-items:stretch;
  perspective:900px;
}
.fm-plate{
  display:grid; grid-template-columns:repeat(7,1fr); gap:2px;
  padding:var(--s-sm); border-radius:var(--r-sm);
  background:var(--canvas); box-shadow:var(--shadow-1), 0 0 0 1px var(--line);
  /* Shallow enough that nine rows of dots still read as nine rows. At 46°
     the plate collapsed to a sagging shelf and the fill was invisible, which
     defeats the one thing the model is for. */
  transform:rotateX(38deg) rotateZ(-4deg) scale(.97);
  transform-origin:50% 60%;
  transition:transform var(--d-mid) var(--ease), box-shadow var(--d-mid) var(--ease);
}
.fm-floor:hover .fm-plate,.fm-floor:focus-visible .fm-plate{
  transform:rotateX(30deg) rotateZ(-4deg) scale(1);
  box-shadow:var(--shadow-2), 0 0 0 1px var(--line);
}
.fm-floor.on .fm-plate{
  transform:rotateX(24deg) rotateZ(-4deg) scale(1.03);
  box-shadow:var(--shadow-2), 0 0 0 2px var(--ink);
}
.fm-dot{ height:7px; border-radius:2px; background:var(--v-free); }
.fm-dot.on{ background:var(--c,var(--body)); }
.fm-dot.aisle{ background:none; }
.fm-floor-lab{
  display:flex; align-items:baseline; justify-content:space-between; gap:var(--s-sm);
  font-size:var(--t-cap); line-height:var(--t-cap-lh);
  font-weight:var(--w-strong); color:var(--mute);
}
.fm-floor.on .fm-floor-lab{ color:var(--ink); }
.fm-floor-lab b{ font-weight:var(--w-strong); font-variant-numeric:tabular-nums; }

/* --------------------------------------------- floors, on a phone */

.fm-floors{
  display:grid; grid-template-columns:repeat(4,1fr); gap:var(--s-sm);
  margin-block-end:var(--s-md);
}
.fm-floor-btn{
  border:0; cursor:pointer; font:inherit; text-align:center;
  min-height:var(--touch);
  padding:var(--s-sm) var(--s-xs);
  border-radius:var(--r-md);
  background:var(--canvas-soft); color:var(--body);
  display:grid; gap:4px; justify-items:center;
  transition:background var(--d-fast) var(--ease), color var(--d-fast) var(--ease);
}
.fm-floor-btn.on{ background:var(--ink); color:var(--primary); }
.fm-floor-btn b{ font-size:var(--t-sm); line-height:var(--t-sm-lh); font-weight:var(--w-strong); }
.fm-floor-btn .fm-fill{
  width:100%; height:4px; border-radius:var(--r-pill);
  background:var(--canvas); overflow:hidden;
}
.fm-floor-btn.on .fm-fill{ background:var(--body); }
.fm-floor-btn .fm-fill i{ display:block; height:100%; background:var(--positive); }

/* ------------------------------------------------------- legend */

.fm-legend{ display:flex; flex-wrap:wrap; gap:var(--s-sm); margin-block-start:var(--s-md); }
.fm-key{
  display:inline-flex; align-items:center; gap:6px;
  border:0; cursor:pointer; font:inherit;
  min-height:32px; padding:var(--s-xs) var(--s-md);
  border-radius:var(--r-pill);
  background:var(--canvas-soft); color:var(--body);
  font-size:var(--t-cap); line-height:var(--t-cap-lh); font-weight:var(--w-strong);
  transition:background var(--d-fast) var(--ease), color var(--d-fast) var(--ease);
}
.fm-key.on{ background:var(--ink); color:var(--canvas); }
.fm-key i{
  width:12px; height:12px; border-radius:3px; background:var(--c,var(--v-free));
  flex:none; box-shadow:0 0 0 1px var(--line);
}
.fm-key.dashed i{ background:none; box-shadow:inset 0 0 0 1px var(--line); }

/* --------------------------------------------------- confirm bar */

/* شريط التأكيد، كما في تطبيق الطيران: لا يظهر حتى تختار، وحين يظهر يقول
   بالضبط أين اخترت. */
.fm-confirm{
  position:sticky; inset-block-end:0; z-index:60;
  margin-block-start:var(--s-md);
  margin-inline:calc(var(--pad) * -1);
  padding:var(--s-md) var(--s-lg) calc(var(--s-md) + var(--sab));
  background:var(--ink); color:var(--canvas);
  display:flex; align-items:center; gap:var(--s-md); flex-wrap:wrap;
  box-shadow:var(--shadow-3);
}
.fm-confirm .fm-pick{ flex:1; min-width:0; }
.fm-confirm .fm-code{
  font-size:var(--t-lg); line-height:var(--t-lg-lh); font-weight:var(--w-hero);
  direction:ltr; font-variant-numeric:tabular-nums;
}
.fm-confirm .fm-where{
  font-size:var(--t-cap); line-height:var(--t-cap-lh); color:var(--canvas-soft);
}
@media(min-width:900px){
  .fm-confirm{ margin-inline:0; border-radius:var(--r-xl); }
}

/* --------------------------------------------------- depth picker */

/* ثلاثة صناديق خلف بعضها. C أقربها إليك عند الممر، وA عند الجدار. الترتيب
   المرئي يقول ذلك بما لا يقوله أي عنوان مكتوب. */
.fm-depths{ perspective:700px; display:grid; gap:var(--s-sm); padding-block:var(--s-md); }
.fm-depth{
  --c:var(--v-free);
  border:0; cursor:pointer; font:inherit; text-align:start;
  min-height:56px; padding:var(--s-md) var(--s-lg);
  border-radius:var(--r-lg);
  background:var(--c); color:var(--ink);
  box-shadow:var(--shadow-1);
  display:flex; align-items:center; justify-content:space-between; gap:var(--s-md);
  transform-style:preserve-3d;
  transition:transform var(--d-mid) var(--ease), box-shadow var(--d-mid) var(--ease);
  opacity:0; animation:fm-rise var(--d-mid) var(--ease) forwards;
}
.fm-depth[data-d="C"]{ transform:translateZ(26px)  scale(1.02); animation-delay:0ms;   z-index:3 }
.fm-depth[data-d="B"]{ transform:translateZ(0)     scale(.99);  animation-delay:60ms;  z-index:2 }
.fm-depth[data-d="A"]{ transform:translateZ(-26px) scale(.96);  animation-delay:120ms; z-index:1 }
.fm-depth.free{ background:transparent; box-shadow:inset 0 0 0 2px var(--line); }
.fm-depth:hover,.fm-depth:focus-visible{ box-shadow:var(--shadow-2); }
.fm-depth:focus-visible{ outline:2px solid var(--ink); outline-offset:2px; }
@keyframes fm-rise{
  from{ opacity:0; transform:translateY(10px) }
  to  { opacity:1 }
}
/* The keyframe cannot carry each depth's translateZ, so the animation only
   fades in and the transform rule above owns the position. */
.fm-depth{ animation-name:fm-fade }
@keyframes fm-fade{ from{ opacity:0 } to{ opacity:1 } }

.fm-depth .fm-dlab{ font-size:var(--t-body); font-weight:var(--w-strong); }
.fm-depth .fm-dsub{ font-size:var(--t-cap); color:var(--body); }
.fm-depth .fm-dcode{
  direction:ltr; font-variant-numeric:tabular-nums;
  font-size:var(--t-sm); font-weight:var(--w-strong);
}

/* ------------------------------------------------------- responsive */

/* الشاشات الكبيرة: النموذج ثلاثي الأبعاد إلى جانب الخريطة، والسطح مائل قليلاً
   حتى تُقرأ الطبلية كصندوق. */
@media(min-width:900px){
  .fm{ --fm-seat:44px; --fm-gap:6px; }
  .fm-body{ display:flex; gap:var(--s-xl); align-items:flex-start; }
  .fm-main{ flex:1; min-width:0; }
  .fm-deck{ --fm-rx:11deg; }
  .fm-slot{ font-size:var(--t-cap); }
  .fm-floors{ display:none; }
}

/* شريط التأكيد على الجوال ثابت لا لاصق.
   sticky يثبّت داخل حدود أبيه فقط: ما إن ينتهي صندوق الخريطة تحت طيّة الشاشة
   حتى ينزل الشريط معه ويختفي — وأنت في منتصف اختيار موقع. الطيران يثبّته،
   ونحن كذلك. */
@media(max-width:899px){
  .fm.placing .fm-confirm{
    position:fixed; inset-inline:0; inset-block-end:0; margin-inline:0;
  }
  /* وإلا اختفى آخر صف من المقاعد خلفه */
  .fm.placing{ padding-block-end:96px; }
}

/* الجوال: بلا منظور إطلاقاً. شبكة مائلة على 375 بكسل شبكة لا يصيبها أحد. */
@media(max-width:899px){
  .fm{ --fm-seat:42px; --fm-gap:4px; }

  /* منتقي الثلاجة شريط أفقي لا أربع بطاقات فوق بعضها: هو خطوة واحدة قبل
     الخريطة، وأربع بطاقات كاملة تعني تمرير شاشة ونصف قبل رؤية أي مقعد. */
  .fm-fridges{
    display:flex; gap:var(--s-sm); overflow-x:auto; scroll-snap-type:x mandatory;
    scrollbar-width:none; margin-inline:calc(var(--pad) * -1);
    padding-inline:var(--pad); padding-block-end:var(--s-xs);
  }
  .fm-fridges::-webkit-scrollbar{ display:none }
  .fm-fridges > *{ flex:0 0 auto; min-width:9.5rem; scroll-snap-align:start }
  .fm-fridges .kpi .val{ font-size:var(--t-h3); line-height:var(--t-h3-lh) }
  .fm-fridges .kpi .sub{ display:none }
  .fm-tower{ display:none; }
  .fm-stage{ perspective:none; }
  .fm-deck{ --fm-rx:0deg; transform:none; }
  .fm-slot:hover:not(.blocked),.fm-slot:focus-visible{ transform:scale(1.06); }
  .fm-slot.sel{ transform:scale(1.08); }
  .fm-rowno{ min-width:18px; }
}

@media(max-width:380px){
  .fm{ --fm-seat:38px; --fm-gap:3px; }
}

/* لا تُطبع الخريطة: الغرض منها اللمس، وعلى الورق تصبح مربعات رمادية. */
@media print{ .fm{ display:none } }

/* ------------------------------------------- اختيار ملصقات الأرفف
   The dialog behind «طباعة ملصقات الأرفف» — filters, then a checklist of
   rack codes grouped by دور. It lives here rather than in app.css because
   nothing outside the fridge map opens it, and in label.css it would be
   wrong twice over: that sheet is the PRINTED artwork, and this is a screen
   the printed artwork never sees. */
.rackpick{ display:flex; flex-direction:column; gap:var(--s-md) }
.rackpick-f{ display:flex; flex-direction:column; gap:var(--s-xs) }
/* Sticky, because the count and «تحديد المعروض» are what the ticking below
   is aimed at — scrolling seventy-two codes away from them turns a two-tap
   job into a scroll-tap-scroll one. */
.rackpick-bar{
  position:sticky; top:0; z-index:1;
  background:var(--canvas); padding-block:var(--s-xs);
  gap:var(--s-sm); flex-wrap:wrap;
}
.rackpick-grid{ display:flex; flex-direction:column; gap:var(--s-md) }
.rackpick-floor{ display:flex; flex-direction:column; gap:var(--s-xs) }
/* The codes are Latin-and-digits in an RTL sheet; tabular figures keep the
   grid from shuffling a column's width as ticks change nothing about it. */
.rackpick-floor .chip{ font-variant-numeric:tabular-nums; min-height:36px }
/* One line per side inside a دور: يمين and يسار are two different walls, and
   letting them wrap into each other puts the first rack of one wall at the
   end of the other's line — a mis-tap waiting to happen on a phone. */
.rackpick-side{ display:flex; align-items:center; gap:var(--s-sm) }
.rackpick-side > span{ flex:0 0 2.6rem }
.rackpick-side .chips{ flex:1 1 0; min-width:0 }


/* ==== packages/ui/label.css ==== */
/* ============================================================
   packages/ui/label.css — ملصقات الطبليات والرفوف
   ============================================================
   Two named page boxes, and nothing else. A sticker is not a report: it has
   no letterhead, no margins, and no second page — it is a 60×40mm rectangle
   that a thermal printer feeds one at a time, and every millimetre of it is
   spoken for.

   ⚠ THE PRINT DIALOG MUST BE SET ONCE, BY HAND, ON EACH MACHINE:
       • «الهوامش: بلا» / Margins: None
       • «المقياس: 100%» / Scale: 100%  (NOT «ملاءمة الصفحة» / Fit to page)
       • «رؤوس وتذييلات الصفحة»: OFF / Headers and footers: unticked
         — on by default in Chrome, and its band is about 8mm. That is a
         FIFTH of the label's height taken out of the page box, and it is
         what printed one sticker across two labels, 80% then 20%. The .lbl
         rule below now shrinks rather than fragments, but shrinking costs
         QR module size; unticking this costs nothing.
     The browser's default is "fit to page", which shrinks 60mm of artwork to
     ~55mm and takes the QR's module size below what a hand scanner in a cold
     store will resolve. The label then prints, looks fine to a human, and
     does not scan. This is the single most common failure on these labels,
     and no amount of CSS can defend against it — @page margin:0 is only a
     REQUEST, and the scale factor is not exposed to the page at all.

     ⚠ THIS APPLIES TO THE PRINT-DIALOG PATH ONLY. A sticker sent to the
     Zebra is a bitmap addressed in printer dots (apps/storage/zebra.js), so
     there is no dialog, no scale factor and nothing to set by hand. The
     warning stands because the dialog is the fallback whenever the printer
     is not reachable — see STORAGE.printLabelDialog().

   Colour: like print.css, everything here lives inside @media print and uses
   #000/#fff literally. That is deliberate, not an oversight of the token
   rule — a thermal label printer has exactly two states, burn and no-burn,
   and a token that resolves to a near-black would be dithered into a grey
   the head cannot render crisply. The design-token test scopes itself to the
   live (screen) part of the sheet for exactly this reason.

   Weights are 900 and 600 only, on paper as on screen.
   ============================================================ */

/* Hidden on screen, exactly like #print-root. The label document is built
   into the same host, so this file adds no screen surface at all. */

/* THE PAGE BOXES ARE NOT HERE ANY MORE. They were two named @page rules —
   `@page label{size:60mm 40mm}` and `@page rack{size:50mm 100mm}` — pointed at
   with the `page` property. That is correct CSS, and current Chrome honours it:
   Page.printToPDF returns 50.1 × 99.8mm for a rack run, against 209.9 × 297
   with the named rules removed. But WebKit has never implemented `page` at all,
   and this system prints stickers from phones. On an engine that ignores it,
   BOTH stickers fell silently through to print.css's unnamed A4 rule below them
   in the bundle — 16mm of margin on a 40mm label — and nothing said so.

   REPORT.doPrint() now emits one unnamed @page per run instead; the two boxes
   live beside the code that uses them, in apps/storage/label.js. */

@media print{

  /* The user-agent's own 8mm body margin. @page{margin:0} governs the SHEET;
     it does nothing about the margin the browser puts on <body>, and on a
     50mm label that margin is a sixth of the width — enough to push the
     artwork off the stock even when the dialog is set correctly. Zeroed for
     the whole print sheet, which is safe: every printed document in this
     system is built into #print-root and sizes itself. */
  html, body{ margin:0; padding:0 }

  /* --- the page box swap ---
     body.printing already hides #shell and shows #print-root (print.css).
     These two classes only re-point the page box, so a sticker run reuses
     the whole existing print mechanism including its cleanup. */
  /* The classes stay: they gate the print-only rules below, and doPrint()'s
     teardown list is load-bearing. What they no longer do is re-point a named
     page box — see the note at the head of this file. */

  .labeldoc, .rackdoc{
    color:#000; background:#fff;
    line-height:1.15;
    /* The digits stay LTR and tabular even though the sheet is RTL. A pallet
       count rendered backwards is a mis-pick. */
    font-variant-numeric:tabular-nums;

    /* Some engines drop background and border colour as "background
       graphics" unless told otherwise. The hairline rule on this sticker is
       a border and would survive, but a dropped rule is invisible until a
       hundred labels are on a roll. */
    print-color-adjust:exact; -webkit-print-color-adjust:exact;

    /* An explicit stack ending in system Arabic faces.
       .lbl-variety is nowrap + ellipsis, so a print job that renders before
       a webfont resolves does not merely look different — the fallback is
       wider, and the variety silently truncates. On a sticker, a truncated
       variety is a mis-pick.

       INTER LEADS IT, and it was missing. tokens.css splits the two faces by
       unicode-range, so Inter can only ever take the digits and Arabic still
       falls through to Noto — but without it named here the digits came from
       "Segoe UI" on a desktop and from whatever a phone has. That did not
       matter while the sticker was rendered on the machine looking at it; it
       matters now that apps/storage/zebra.js paints the same sticker into a
       bitmap on one device and prints it on another, and a printed artefact
       must not depend on who pressed the button. */
    font-family:"Inter", "Noto Sans Arabic", "Segoe UI", Tahoma, "Arial", sans-serif;
  }
  .labeldoc bdi, .labeldoc .num,
  .rackdoc  bdi, .rackdoc  .num{
    unicode-bidi:isolate; direction:ltr; font-variant-numeric:tabular-nums;
  }

  /* ---------------------------------------------------- pallet sticker */

  /* One sticker = one page. break-after:page on every label makes N copies
     feed as N stickers rather than N labels crammed onto one 40mm page.
     The last one still carries it; a trailing blank page costs one label on
     a roll and is far cheaper than a run that silently drops the final
     sticker on printers that ignore break-after on the last child.

     TWO COLUMNS, row-reverse: identity on the physical left, facts on the
     right. It was three horizontal bands carrying a date, a location and a
     typed code as well; the owner asked for exactly this and nothing more.
     60 − .8 of border − 3.6 of padding − 1.5 of gap = 54.1mm, split
     25 / 29.1.

     IT WAS 50mm WIDE. The stock in the building is 60×40mm, so a 50mm
     sticker left a sixth of every label blank. The 10mm went to the two
     things that were short: the QR frame (23 → 25mm) and the facts column
     (21.1 → 29.1mm), which is the one .lbl-variety truncates in. 60mm is
     also 480 printer dots at 203dpi exactly, where 50mm was 399.6 — that
     matters to zebra.js and to nothing else here. */
  /* ⚠ THE SIZE IS A CEILING, NOT AN ASSERTION — and that is the whole of the
     «one sticker printed across two labels» fix.

     The artwork adds up to 40.000mm inside a 40.000mm page: 40 − .8 of border
     − 3.6 of padding = 35.60 of content, and the facts column spends
     6.00 + .80 + 28.80 = 35.60 of it. EXACTLY. Not «under» — equal, to three
     decimal places.

     A box that exactly equals its page is fine until the page loses a
     millimetre, and a page loses millimetres for reasons no stylesheet can
     see: a driver's hardware margins, a dialog set to «افتراضية» rather than
     «بلا», and — on by default in Chrome — the headers-and-footers band, which
     is about 8mm. `break-inside:avoid` then does what the spec says: it pushes
     the box whole to the next page, and when it does not fit there either it
     fragments. 32mm of artwork on one label and 8mm on the next is 80% / 20%,
     which is what was reported, to the percentage point.

     So the sticker now says «60 × 40mm, AND NEVER MORE THAN THE PAGE». In
     paged media vw/vh resolve against the page box, so on a correct 60 × 40
     page these change nothing whatsoever — and on a short one the label
     shrinks onto ONE label instead of splitting across two. Degrade, never
     fragment: a sticker printed small is a sticker somebody can reprint, and
     a sticker in two pieces is two wasted labels and a pallet with no number.

     The QR frame and the ident column are allowed to shrink with it (see
     .lbl-ident and .lbl-qrbox below), so what gives way is scale rather than
     content — nothing is clipped, and the 10mm ruler on the test sticker is
     what reveals it happened. */
  .lbl{
    box-sizing:border-box;
    width:60mm; height:40mm;
    max-width:100vw; max-height:100vh;
    /* The outer frame the design opens with. It is not decoration on a
       thermal label: a die-cut sticker is trimmed with a tolerance of a
       millimetre or so, and a printed border is what tells the operator at a
       glance that the whole artwork landed on the stock rather than half a
       millimetre off its edge. 2mm of padding sits inside it. */
    border:.4mm solid #000; border-radius:2mm;
    padding:1.8mm;
    display:flex; flex-direction:row-reverse; align-items:stretch; gap:1.5mm;
    overflow:hidden;
    break-after:page; break-inside:avoid;
  }
  .lbl:last-child{ break-after:auto }

  /* ---- العمود الأيسر: الهُوية ----
     row-reverse on .lbl puts this column on the physical LEFT of the
     sticker, which is the owner's spec and also where the QR has always
     sat — a hand scanner is aimed at the same edge of every label in the
     building, and a picker never has to work out which way round it went on.

     25mm of the 54.1mm of content — and the sticker's WIDTH is not what set
     it. The column is 35.6mm tall (40 less .8 of border and 3.6 of padding),
     and a ~8.7mm number block plus 1mm of gap leaves 25.9 for the frame:
     8.7 + 1 + 25 = 34.7 of 35.6. The vertical budget caps the QR, which is
     why widening the sticker further would not grow it. */
  .lbl-ident{
    flex:0 0 25mm; width:25mm; min-width:0;
    /* Shrinkable, so a page box shorter than 40mm takes it out of the QR's
       SIZE rather than out of its bottom edge. Without this the column's
       8.7mm number plus its 25mm frame stay rigid and `overflow:hidden` on
       .lbl cuts the code in half — which scans no better than not printing
       it at all. */
    min-height:0;
    display:flex; flex-direction:column; align-items:center;
    justify-content:space-between; gap:1mm;
  }
  /* 26pt, and it is the whole point of the sticker. The size is set INLINE
     by lblNoSize() in label.js, which now MEASURES the number against the
     24mm it has rather than stepping by digit count, because no CSS shrinks
     text to fit its box. This declaration is the ceiling the inline value
     starts from and the floor a sticker whose script somehow did not run
     still prints — a legible number rather than 16px of body text. */
  .lbl-no{
    font-size:26pt; font-weight:900; line-height:.95;
    white-space:nowrap; flex:0 0 auto;
  }
  /* The framed QR. 25mm outer, less a .5mm border and 1mm of padding, is
     22mm of code — 0.88mm per module at 25 modules, up from 0.80 when the
     same frame sat in a 23mm column on 50mm stock. That is back to what the
     code had before it was framed at all, and it now has the frame too.

     On the Zebra path the module is a whole 7 dots (0.875mm) rather than
     7.04, and a whole module is worth more than the 0.005mm it gives back:
     a resampled module carries a grey edge into a head that has only burn
     and no-burn, and a whole one does not.

     This is the first number to move if a scan rate ever drops, and it can
     only move by taking height from .lbl-no. Do not shrink it. */
  /* `flex:0 1 auto` and a square aspect: 25 × 25mm whenever there is room —
     which is every correctly-set page — and a smaller SQUARE when there is
     not. A QR that shrinks still scans; a QR with its bottom third missing
     does not. `width:auto` against a definite height and `aspect-ratio:1`
     keeps the two sides equal as it gives way. */
  .lbl-qrbox{
    box-sizing:border-box;
    height:25mm; width:auto; aspect-ratio:1 / 1;
    flex:0 1 auto; min-height:0;
    border:.5mm solid #000; border-radius:1.5mm; padding:1mm;
    display:flex; align-items:center; justify-content:center;
  }
  /* 100% of a 25mm frame less .5 of border and 1mm of padding IS 22mm, so
     this is the same number it has always been — written so it follows the
     frame down instead of standing still while the frame shrinks under it. */
  .lbl-ident svg{ width:100%; height:100%; display:block }

  /* ---- العمود الأيمن: الحقائق ----
     A header pill naming the variety, then three rows: boxes, kilograms,
     date. The vertical rule between the columns is a BORDER on this column,
     not a filled element — a thermal head renders a .3mm rule reliably and a
     hairline rectangle is a coin toss between a line and nothing.

     55.6 − 25 of ident − 1.5 of gap − 1.2 of padding − .3 of rule = 27.6mm. */
  .lbl-facts{
    flex:1 1 auto; min-width:0;
    padding-inline-start:1.2mm;
    border-inline-start:.3mm solid #000;
    display:flex; flex-direction:column; gap:.8mm;
    text-align:start;
  }

  /* الصنف والدرجة — سطر واحد في المنتصف، فوق خطّ.
     IT WAS A PILL WITH A PALM ICON IN IT. The pill said "title" and the icon
     said nothing, and between them they took 8.4mm of a 27.6mm column away
     from the one string on the sticker with no fixed length — a variety was
     ellipsised on a label that had room for it. A centred line over a rule
     says the same thing for the price of the rule.

     6mm of the 35.6mm column. The font size is set INLINE by lblHead() in
     apps/storage/label.js, measured against 26.5mm; the value here is the
     ceiling it starts from and the floor a sticker whose script did not run
     still prints. */
  .lbl-head{
    flex:0 0 6mm; height:6mm; min-width:0;
    border-block-end:.35mm solid #000;
    display:flex; align-items:center; justify-content:center;
  }
  /* nowrap + ellipsis is the LAST resort, not the mechanism: lblFitPt()
     shrinks to 6pt before this can bite. It stays because a print that
     renders before the webfont resolves measures against a wider fallback
     face, and a variety that overflows its box on a thermal label is a
     mis-pick. */
  .lbl-variety{
    font-size:9.5pt; font-weight:900; line-height:1.05; text-align:center;
    min-width:0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap;
  }

  /* Four rows sharing whatever the header leaves: 35.6 − 6 − .8 = 28.8mm,
     7.2mm each. flex:1 1 0, so a pallet counted in boxes — no kilogram row —
     or one with no entered_on fills the same column with three rows and
     leaves no hole. */
  .lbl-rows{
    flex:1 1 auto; min-height:0;
    display:flex; flex-direction:column;
  }
  .lbl-row{
    flex:1 1 0; min-height:0; min-width:0;
    display:flex; align-items:center; gap:.8mm;
  }
  .lbl-row + .lbl-row{ border-block-start:.2mm solid #000 }
  .lbl-lab{
    flex:0 0 auto; display:flex; align-items:center; gap:.8mm;
    font-size:7pt; font-weight:600; white-space:nowrap;
  }
  .lbl-row .lbl-ico{ width:3.4mm; height:3.4mm; flex:0 0 auto }

  /* THERE IS NO .lbl-dot ANY MORE. A column of dots stood between every
     label and its value — a leader, rendered as a repeating radial-gradient
     because a sub-millimetre `dotted` border is rounded to a solid hairline
     by every print engine tried. The owner asked for it out of every row:
     on a 7.2mm row it reads as a fourth vertical rule rather than as a link,
     and `margin-inline-start:auto` on the value already does the work the
     leader was there to do. zbDotColumn() lost its pallet caller with it. */

  /* The values are the reason the row exists, so they are the heaviest thing
     on it and they are pushed to the far edge where the eye scans down a
     column of them. The size is set INLINE by lblRow(), fitted to whatever
     the icon and the Arabic label leave — 10pt for a quantity, 8 for a date
     or a supplier, down to 5.5 before anything is ellipsised. This is the
     ceiling, and the floor for a sticker whose script did not run. */
  .lbl-val{
    margin-inline-start:auto; min-width:0;
    font-size:10pt; font-weight:900; line-height:1;
    white-space:nowrap; overflow:hidden; text-overflow:ellipsis;
  }
  /* Kept as hooks: the tests and tools/browser/check.mjs address the rows by
     these names, and so does anyone reading a diff of a printed sticker.
     The date and the supplier no longer carry a font size of their own —
     lblFitPt() sizes both, which is what let «التاريخ» stop being a special
     case with an essay attached. */
  .lbl-boxes, .lbl-kg, .lbl-date, .lbl-supplier{ min-width:0 }

  /* ------------------------------------------------------ rack labels */

  /* 50×100mm PORTRAIT, ONE RACK TO A PAGE.
     It was four cells on a 70×40mm landscape sheet — 32.40 × 17.40mm each,
     with a 14mm QR at 0.56mm per module: the smallest code in the building,
     on the label read from furthest away. The label COUNT is unchanged (72
     racks were 18 sheets × 4; they are now 72 pages × 1), only the stock.
     What changed is the code: a 31.7mm frame holding 28.15mm of QR is 1.126mm
     per module.

     THE CARD PAID FOR THE CODE. The chips were 8mm holding a 5mm icon in a
     20.1mm card; at 6.5mm holding 4mm — the size the spec asked for and the
     size that stops the icon reading as the row's subject — the card is
     19mm and the QR frame grew from 28 to 31.7. 0.86 → 1.126mm per module,
     on the label read from furthest away, from a forklift.

     The vertical budget, and it is checked in tools/browser/check.mjs
     because arithmetic is necessary and not sufficient:
       2.0 padding + 6.5 badge + 19.0 card + 31.7 qr + 5.5 location
       + 7.0 depths + 8.0 bar          = 79.7mm of fixed bands
       + 2.0 + 2.0 + 1.5 + 1.5 + 1.0   =  8.0mm of margins between them
       = 87.7mm, leaving .rack-id 12.3mm.

     AND THAT IS THE WHOLE OF IT — there is no margin between the tab and the
     ID. .rack-label is a column flex container with NO row-gap and .rack-id
     has no margin-block-start, so the 2.0 an earlier version of this comment
     counted there does not exist. The band budget was never wrong on paper —
     nothing overflows, which is all tools/browser/check.mjs §14b could see —
     but it under-reported the slack by 2mm and named 10.0 for a band that
     renders at 12.3. §14b now measures the band and says so.

     .rack-id is the only `flex:1 1 auto`, so ALL of the slack is its and
     every engine's rounding lands in the one place that can absorb it
     instead of pushing the bottom bar off the sheet. */
  /* THE SHEET IS LANDSCAPE AND THE LABEL IS NOT.
     The roll goes through the printer with its 100mm edge across the head and
     its 50mm edge along the feed, so the PAGE is 100 × 50mm — and the artwork,
     which is designed and die cut 50 × 100, is turned a quarter clockwise to
     sit on it. apps/storage/zebra.js does the same thing to the same artwork
     by permuting dots; this is the dialog's half of it.

     `transform-origin:0 0` then `rotate(90deg)` maps the 50 × 100 box onto
     x ∈ [−100, 0], y ∈ [0, 50]; the translate brings it back. CSS applies the
     list right to left, so the rotate happens first and the translate is in
     the ROTATED frame's terms — which is why it is written this way round and
     not the other.

     The label keeps its own 50 × 100 layout box, untransformed, which is what
     lets every band inside it stay written in the millimetres the design is
     drawn in — and what lets tools/browser/check.mjs measure it by switching
     one property off. */
  .rack-sheet{
    box-sizing:border-box;
    width:100mm; height:50mm;
    position:relative; overflow:hidden;
    break-after:page; break-inside:avoid;
  }
  .rack-sheet:last-child{ break-after:auto }
  .rack-sheet > .rack-label{
    position:absolute; inset-block-start:0; inset-inline-start:0; left:0;
    transform:translateX(100mm) rotate(90deg);
    transform-origin:0 0;
  }

  .rack-label{
    box-sizing:border-box;
    width:50mm; height:100mm;
    padding:2mm 2mm 0;
    border-radius:3mm;
    background:#fff; color:#000;
    display:flex; flex-direction:column; align-items:center;
    overflow:hidden;
    /* The page break belongs to .rack-sheet now — the label is absolutely
       positioned inside it and breaking on it too would feed a blank. */
    break-inside:avoid;
  }

  /* --- the tab, and the two rules beside it --- */
  .rack-top{
    flex:0 0 6.5mm; width:100%;
    display:flex; align-items:center; gap:1.5mm;
  }
  /* Hairlines, and they stop short of the tab on both sides — a rule that
     touches the black block reads as a leader line into it. */
  .rack-rule{ flex:1 1 0; height:.3mm; background:#000 }
  .rack-badge{
    flex:0 0 16mm; height:7mm;
    /* -2mm eats the page padding so the tab meets the trimmed top edge,
       which is what makes it read as a tab rather than a floating box. */
    margin-block-start:-2mm;
    background:#000; border-radius:0 0 2mm 2mm;
    display:flex; align-items:center; justify-content:center;
  }
  /* النخلة. A FILLED inline SVG, not the company logo and not an <img>.
     It was REPORT.LOGO_SRC inverted to white, which is a file that has to
     load before the dialog opens — and a tab whose artwork has not arrived
     prints as an empty black block, which is exactly what the owner was
     looking at. Inline path data cannot fail to load.

     Filled, not stroked like the row icons: at 4.6mm a 0.3mm outline of a
     palm is a grey smudge on a thermal head, and a solid silhouette knocked
     out of the black tab is the one shape that always survives. */
  .rack-emblem{
    width:4.6mm; height:5mm; display:block; fill:#fff;
  }

  /* --- the hero --- */
  /* 10mm of band, ~28pt. The spec asks 16mm and then asks for a 31.5mm QR
     as well; the two cannot both be had inside 100mm, and the QR is worth
     more than the digits — a code that will not scan costs a walk back to
     the office, a smaller number costs a half-step closer to the upright.
     flex:1 1 auto — this band, and only this band, absorbs slack. */
  .rack-id{
    flex:1 1 auto; min-height:10mm;
    display:flex; align-items:center; justify-content:center;
    font-size:28pt; font-weight:900; line-height:1; letter-spacing:.02em;
    white-space:nowrap;
  }

  /* --- the information card --- */
  /* .6 border ×2 + 1.4 padding ×2 + 7.4 row ×2 + .4 divider = 19.0mm. */
  .rack-card{
    flex:0 0 19mm; width:100%; box-sizing:border-box;
    margin-block-start:2mm;
    border:.6mm solid #000; border-radius:2mm; padding:1.4mm;
    display:flex; flex-direction:column;
  }
  .rack-crow{
    flex:1 1 0; min-height:0; min-width:0;
    display:flex; align-items:center; gap:1.2mm;
  }
  .rack-crow + .rack-crow{ border-block-start:.4mm solid #000 }
  /* 6.5mm holding a 4mm icon, and the 1.25mm of ground on every side is the
     point: at 8mm holding 5mm the chip was the loudest thing in the card and
     the words beside it read as its caption. Both rows are the same size to
     the tenth — two chips that differ read as two kinds of thing. */
  .rack-chip{
    flex:0 0 6.5mm; width:6.5mm; height:6.5mm;
    background:#000; border-radius:1.3mm;
    display:flex; align-items:center; justify-content:center;
  }
  .rack-chip .lbl-ico{ width:4mm; height:4mm }
  .rack-ctext{
    flex:0 1 auto; min-width:0;
    font-size:10.6pt; font-weight:900; line-height:1.05;
    white-space:nowrap; overflow:hidden; text-overflow:ellipsis;
  }
  /* A repeating radial-gradient, not a `dotted` border: a sub-millimetre
     dotted border is rounded to a solid hairline by every print engine
     tried, and the divider becomes a second rule.

     5mm AND CENTRED, not `align-self:stretch`. Stretched, it ran the full
     7.4mm of the row and met the card's own rules top and bottom, which is
     how a link between two fields turns into a column border. This is the
     one dotted separator left in the system — the pallet sticker's went out
     of every row at the owner's ask. */
  .rack-dot{
    flex:0 0 .6mm; align-self:center; height:5mm; margin-inline:.8mm;
    background-image:radial-gradient(#000 45%, transparent 47%);
    background-size:.6mm 1.6mm; background-repeat:repeat-y;
  }
  .rack-pill{
    margin-inline-start:auto; flex:0 0 auto;
    height:5.5mm; min-width:10mm; border-radius:1.5mm; padding-inline:1.5mm;
    background:#000; color:#fff;
    display:inline-flex; align-items:center; justify-content:center;
    font-size:9pt; font-weight:900; line-height:1; white-space:nowrap;
  }

  /* --- the code --- */
  /* 31.7mm outer − .7 border ×2 − 1 padding ×2 = 28.1mm of content, and the
     code is 28.15 over 25 modules = 1.126mm per module. Up from 0.86 when the
     frame was 28mm with a 1.2mm border and 2mm of padding, and up from the
     1.10 this rule gave while the frame was 31.5 and the svg 27.5. Treat the
     31.7 as a floor.

     IT WAS 31.5, AND THE 0.2mm IS THE ZEBRA'S. On the dot path the QR module
     must be a WHOLE number of printer dots — the entire argument in
     apps/storage/zebra.js — and 31.5mm is 251.75 dots, which after .7 of
     border and 1 of padding each side leaves 224.58 for 25 modules: 8 whole
     dots, and 8 would have shrunk the code to 25.0mm. 31.7 leaves 226.18,
     which is 9 dots — 225 of them, 28.15mm, the number above. The frame grew
     a fifth of a millimetre and the code grew half of one.

     The 0.2 comes out of .rack-id, which is the only band that can give it:
     it is the sole `flex:1 1 auto` here, exactly so that every engine's
     rounding lands in the one place that can absorb it. */
  .rack-qrbox{
    flex:0 0 31.7mm; width:31.7mm; box-sizing:border-box;
    margin-block-start:2mm;
    border:.7mm solid #000; border-radius:2mm; padding:1mm;
    display:flex; align-items:center; justify-content:center;
  }
  .rack-qrbox svg{ width:28.15mm; height:28.15mm; display:block }

  /* --- «الموقع» --- */
  .rack-where{
    flex:0 0 5.5mm; width:100%;
    margin-block-start:1.5mm;
    display:flex; align-items:center; gap:1.5mm;
  }
  .rack-where .rack-pill{ margin-inline-start:0; height:5.5mm }

  /* --- C │ B │ A --- */
  .rack-depth-strip{
    flex:0 0 7mm; width:100%;
    margin-block-start:1.5mm;
    display:flex; align-items:center;
  }
  .rack-depth{
    flex:1 1 0; min-width:0;
    display:flex; align-items:center; justify-content:center;
    font-size:20pt; font-weight:900; line-height:1;
    direction:ltr; unicode-bidi:isolate;
  }
  /* SHORT BARS, NOT FULL-HEIGHT BORDERS. `border-inline-start` ran the whole
     7mm of the strip and read as structure — three columns of a table
     reaching for the الموقع row above and the bar below — rather than as two
     marks separating three letters. 5.5mm of .4mm rule, centred, touching
     neither neighbour: «A | B | C», which is all the strip was ever saying.

     .4mm and not thinner: a thermal head renders a .4mm rule reliably, and
     «B A» with a missing bar reads as one field. */
  .rack-depth + .rack-depth{
    position:relative;
  }
  .rack-depth + .rack-depth::before{
    /* RIGHT, not `inset-inline-start`: the CELL is direction:ltr (the letters
       are Latin), so «start» resolves to its left and the bar landed on the
       outside edge of the strip instead of between two letters. The strip
       itself runs C B A right-to-left, so the bar belongs on each cell's
       right — between C and B, and between B and A. */
    content:""; position:absolute; right:0;
    top:50%; transform:translateY(-50%);
    width:.4mm; height:5.5mm; background:#000;
  }

  /* --- the direction bar --- */
  /* Full bleed: the -2mm margins eat the page padding so the bar reaches
     both trimmed edges, and the bottom radius follows the sticker's own. */
  .rack-foot{
    flex:0 0 8mm; width:50mm;
    margin-block-start:1mm; margin-inline:-2mm;
    background:#000; color:#fff;
    border-radius:0 0 3mm 3mm;
    display:flex; align-items:center; justify-content:center;
    font-size:9.9pt; font-weight:900; line-height:1; white-space:nowrap;
  }

  /* The calibration page is a rack page with nothing on it but the ruler —
     see printTestRackLabel(). */
  .rack-hint-page{ justify-content:center; padding:2mm }

  /* The dialog reminder, printed on the test label only. It exists so the
     person calibrating the printer is holding the instructions. */
  /* flex:0 0 auto so it takes its own height out of the facts column rather
     than squeezing the three rows — the browser check's overflow sweep is
     what caught that on the test label. */
  .lbl-hint{ font-size:5pt; font-weight:600; margin-block-start:.6mm;
             flex:0 0 auto }
  /* المسطرة: عرضها ١٠ مم بالضبط. إن قاسها المستخدم فوجدها أقلّ فالحوار
     يُصغّر الصفحة، وكل رمز في هذه الطبعة مُصغَّر معها. */
  .lbl-ruler{
    width:10mm; height:1.2mm; margin-block-end:.4mm;
    border:.4pt solid #000; border-block-start-width:0;
  }
}


/* ==== packages/ui/print.css ==== */
/* ============================================================
   packages/ui/print.css — الطباعة و PDF
   ============================================================
   PDF in this system IS print. The browser renders Arabic correctly,
   shapes every ligature, and «حفظ بصيغة PDF» is one tap in the print dialog
   on desktop, iOS and Android alike. A JS PDF library would need an embedded
   Arabic face and a shaping engine, would add ~400KB against a CSP that
   forbids CDNs, and would still get RTL ligatures wrong.

   The document that prints is NOT the screen with things hidden. #print-root
   is built fresh by render-print.js and #app-host is taken away, which is
   the only way a report can carry a letterhead and repeat its headers.
   The two-weight rule (900 hero, 600 everything else) holds on paper as
   much as on screen — it is one variable font either way, and a printed 700
   is off the scale the whole system is built on.

   «صفحة ٢ من ٥» — what is and is not achievable here
   --------------------------------------------------
   It is not achievable, and no amount of CSS makes it so. The paged-media
   spec does define it: `@page{ @bottom-center{ content: "صفحة " counter(page)
   " من " counter(pages) } }`. No browser implements @page margin boxes —
   Chrome, Safari and Firefox all ignore them; only dedicated formatters
   (Prince, WeasyPrint, Paged.js) honour them, and every one of those is a
   dependency this project does not take. counter(page) is likewise readable
   from nowhere a stylesheet can put text.

   What the reader actually gets is the BROWSER's own page header/footer,
   which already prints a page number and is on by default in Chrome and
   Safari. It is in Latin numerals and it sits outside the @page margin, so
   it does not collide with anything here. The alternative — measuring
   content in JS and inserting page-break markers to fake a counter — would
   have to re-measure per paper size, per zoom and per engine, and would be
   wrong the first time a font fell back. So: the letterhead identifies
   every page, the totals print at the foot of the last one, and the page
   NUMBER is the browser's job.
   ============================================================ */

/* Hidden on screen, shown on paper. */
#print-root{ display:none }

/* The static default, and it stays a default. REPORT.doPrint() emits an
   unnamed @page of its own into <head> for every run — later in document
   order, so it wins at equal specificity in every engine — and this is what
   a document that somehow reaches the printer without going through
   doPrint() gets. The two must say the same thing: REPORT.PAGE_A4. */
@page{
  size: A4 portrait;
  margin: 16mm 12mm 18mm;
}

@media print{

  /* --- the swap --- */
  body.printing #shell{ display:none !important }
  body.printing #print-root{ display:block }
  /* .print-landscape no longer re-points a named page box — REPORT.doPrint()
     is handed REPORT.PAGE_A4_LAND instead. The class stays because it is what
     doPrint()'s teardown removes and what a landscape sheet is recognised by. */

  /* --- الحبر، واللون الذي يصل الورق فعلاً ---
     print-color-adjust:exact. المتصفّحُ يُسقط الخلفيّاتِ حين يكون مربّع
     «رسوم الخلفية» مطفأً — وهو مطفأٌ افتراضاً في كلّ حوار طباعة — وهذا
     السطرُ هو ما يجعل Chrome و Safari يطبعان ما قُصد. والملصقاتُ
     (.labeldoc/.rackdoc) تفعلها منذ زمن للسبب نفسه.

     والدرجاتُ فاتحةٌ عمداً: طابعةٌ بالأبيض والأسود تصيّرها بياضاً تقريباً
     بدل رمادٍ يطمس الرقمَ تحته — وهو الاعتراضُ الذي مُنعت لأجله التعبئةُ
     أصلاً، وهو باقٍ على حاله. الترتيبُ تحمله الخطوطُ والأوزانُ والفراغ،
     واللونُ يساعد ولا يحمل. */
  .printdoc{
    color:#111; background:#fff;
    font-size:9.5pt; line-height:1.45;
    print-color-adjust:exact; -webkit-print-color-adjust:exact;
  }

  /* --- the watermark ---
     An <img>, not a background-image, and that is load-bearing.
     print-color-adjust governs BACKGROUNDS being dropped as "background
     graphics", and the print dialog's «Background graphics» box is off by
     default. `.printdoc` deliberately does not set print-color-adjust — only
     .labeldoc/.rackdoc do, because a sticker's black block IS the sticker.
     So a background-image watermark would be absent from almost every real
     print job, and nobody would ever report it: a missing watermark looks
     exactly like a page. An <img> is content and prints regardless.

     Honest about position:fixed on paged media: the spec says a fixed box
     repeats on EVERY page, and Chrome and Firefox do that when printing.
     WebKit does not — Safari paints it on the first page only. Both
     outcomes are acceptable here, which is why fixed is used at all: this
     mark must not interrupt reading, so a first-page-only watermark is a
     milder version of the same thing, not a defect. position:absolute would
     be worse in both engines — it would stretch the box and the mark would
     land once, somewhere in the middle of page one.

     .05, a fifth of .vat-stamp's .10, and greyscale on top. «مسودة» is MEANT
     to interrupt — a draft return picked up as final reaches a portal. This
     one must never compete with a figure printed over it. */
  .print-wm{
    position:fixed;
    inset-block-start:50%; inset-inline-start:50%;
    transform:translate(50%, -50%);      /* RTL: inline-start is the right edge */
    width:110mm; height:110mm; object-fit:contain;
    opacity:.05; filter:grayscale(1);
    z-index:0; pointer-events:none;
  }
  /* One pack, one watermark. Eighteen stacked .print-wm at .05 each compound
     to a mark you cannot read a ledger through. */
  .printdoc ~ .printdoc .print-wm{ display:none }
  /* The content sits ABOVE it. Without this the watermark is painted after
     the table (later in the stacking order at the same z-index) and the
     faint mark washes over every figure it crosses. */
  .printdoc > *:not(.print-wm){ position:relative; z-index:1 }

  /* --- letterhead --- */
  .print-head{
    display:flex; align-items:flex-start; justify-content:space-between;
    gap:8mm; padding-block-end:3.5mm; margin-block-end:5mm;
    /* شعرةٌ بلون العلامة فوق خطٍّ رفيع، بدل قضيبٍ أسود بسُمك 1.5pt.
       والسُّمكُ كان يزن أثقل من العنوان الذي يقدّمه. */
    border-block-end:.5pt solid #999;
    box-shadow:0 2.5pt 0 -2pt #9fe870;
    /* Never split the letterhead from the first rows it introduces. */
    break-after:avoid;
  }
  .print-brand{ display:flex; align-items:flex-start; gap:4.5mm; min-width:0 }
  /* Height, not width: the mark is square today, and a wordmark tomorrow
     would keep its cap height rather than shrinking to fit a width box. */
  .print-logo{ height:17mm; width:auto; flex:none }
  .print-co-name{ font-size:13.5pt; font-weight:900; line-height:1.2 }
  .print-co-meta{ color:#555; margin-block-start:1mm }
  .print-co-meta span{ display:block; line-height:1.5 }
  .print-title{ text-align:end; min-width:0 }
  /* العنوانُ هو أكبرُ ما في الصفحة، ويزن أثقل من اسم الشركة: الورقةُ تُسحب
     من ملفّ، والسؤالُ الأوّل «ما هذه الورقة؟» لا «من أصدرها؟». */
  .print-title h1{ font-size:16.5pt; font-weight:900; margin:0; line-height:1.2 }
  .print-title .t-sm{ color:#555; margin-block-start:.8mm }
  .print-params{ color:#555; margin-block-start:1.5mm }

  .print-foot{
    display:flex; justify-content:space-between; gap:8mm;
    margin-block-start:6mm; padding-block-start:2.2mm;
    border-block-start:.4pt solid #bbb; color:#666;
    font-size:7.5pt;
    break-inside:avoid;
  }

  /* --- tables --- */
  .printdoc table{ width:100%; border-collapse:collapse }
  /* The header repeats on every page — page four of a ledger is unreadable
     without it — and the totals print at the foot of the LAST page, which is
     where a total belongs. */
  .printdoc thead{ display:table-header-group }
  .printdoc tfoot{ display:table-footer-group }
  /* رأسُ الجدول: درجةٌ فاتحةٌ من الأخضر بدل رمادِ #eee، وخطٌّ حازمٌ تحته.
     والحرفُ أصغرُ قليلاً ومتباعدٌ — رأسُ عمودٍ يُقرأ مرّةً ثم يُمسح بالعين. */
  .printdoc th{
    background:#f2faec; border-block-end:.9pt solid #2f3a26;
    padding:2mm 2.2mm; text-align:start; font-weight:600; font-size:8pt;
    letter-spacing:.02em; color:#1f2a18;
  }
  .printdoc td{ padding:1.7mm 2.2mm; border-block-end:.25pt solid #dcdcdc;
                vertical-align:top }
  /* تظليلٌ متناوبٌ خفيفٌ جداً: يقود العينَ عبر صفٍّ من تسع خانات، ويختفي
     على طابعةٍ بالأبيض والأسود بدل أن يطمس شيئاً. */
  .printdoc tbody tr:nth-child(even) td{ background:#fafbf9 }
  .printdoc th.num-h, .printdoc td.num-c{ text-align:end }
  .printdoc tr{ break-inside:avoid }
  .printdoc .b{ font-weight:600 }

  /* A group header stranded at the foot of a page with its first row
     overleaf is the classic printed-report defect. */
  .printdoc .rpt-group-head td{
    background:#eef4e8; font-weight:600; padding-block:2.4mm;
    border-block-start:.4pt solid #c9d6be;
    break-after:avoid;
  }
  .printdoc .rpt-subtotal td{ border-block-start:.5pt solid #666; font-weight:600 }
  /* الخطُّ المزدوجُ يبقى — هو ما يقوله محاسبٌ عن الإجمالي منذ قرن — ومعه
     درجةٌ تجعل السطرَ يُلتقط بالعين عند تصفّح ورقةٍ طويلة. */
  .printdoc .rpt-total td{
    border-block-start:1.5pt double #111; font-weight:900;
    background:#f2faec; padding-block:2.2mm;
  }
  .printdoc .rpt-total, .printdoc .rpt-subtotal{ break-before:avoid }

  /* Tone as a BORDER, not a fill: a colour printer is not guaranteed, and a
     grey wash on a mono printer hides the number it was meant to highlight. */
  .printdoc td.tone-bad{  border-inline-start:1.5pt solid #000 }
  .printdoc td.tone-warn{ border-inline-start:.75pt solid #666 }

  /* Numerals stay LTR and tabular on paper too. Without this a print reset
     can undo bdi and 1,234.5 renders backwards beside Arabic. */
  .printdoc bdi, .printdoc .num{
    unicode-bidi:isolate; direction:ltr; font-variant-numeric:tabular-nums;
  }

  /* --- the vocabulary the ad-hoc documents are built from ---
     A pick list, a batch sheet and a stock summary are not reports — they
     have panels and a signature line rather than columns somebody chose.
     These classes are what keeps them looking like the same company
     produced them as the 21 catalogue reports. */
  .doc-sec{ margin-block-start:5mm; break-inside:avoid }
  .doc-sec-h{
    font-size:11pt; font-weight:900; margin:0 0 2.4mm;
    padding-block-end:1.2mm; border-block-end:.5pt solid #c9d6be;
    break-after:avoid;
  }
  /* A section with a long table must be allowed to split — break-inside on
     .doc-sec is for the short panels, and a 300-row table inside one would
     otherwise be pushed whole onto a page it cannot fit. */
  .doc-sec:has(> table){ break-inside:auto }
  .kvline{ display:flex; justify-content:space-between; gap:3mm }

  .doc-sign{
    display:flex; gap:8mm; margin-block-start:8mm; break-inside:avoid;
  }
  .doc-sign-box{ flex:1 }
  .doc-sign-rule{ border-block-end:.5pt solid #000; margin-block-start:10mm }

  .doc-note{ color:#444; margin-block-start:2mm }

  /* A figure on paper: the page's width, never taller than half a landscape
     sheet, and never split — half a picture is no picture. */
  .dsr-fig{ margin:0; break-inside:avoid }
  .dsr-fig img{ display:block; max-width:100%; max-height:120mm; height:auto; margin-inline:auto }

  /* A cell left blank on purpose, with a rule to write on. The pick list is
     filled in at the pallet and keyed in afterwards; a printed figure with
     nowhere to write the real one is how the two stop matching. */
  .fill-cell{ display:block; min-width:22mm; min-height:5mm;
              border-block-end:.25pt dotted #666 }

  /* --- the pack ---
     Eighteen reports, one document, each starting on a fresh page. The
     cover is a page of its own. */
  .printdoc.pack-part{ break-before:page }
  .pack-cover .print-title h1{ font-size:19pt }

  h1,h2,h3{ break-after:avoid }
  p{ orphans:3; widows:3 }
  a[href]::after{ content:none }        /* no URLs printed after links */
}

/* ============================================================
   الفاتورة الضريبية والإقرار — مستندان لهما شكل تفرضه الجهة
   ============================================================ */
@media print{

  .invoice .inv-head{ border-block-end-width:2pt }
  .invoice .print-title h1{ font-size:17pt }

  /* Seller · buyer · invoice, three blocks that must never split across a
     page — a buyer's tax number continuing overleaf is the one detail an
     inspector looks for first. */
  .inv-meta{
    display:grid; grid-template-columns:repeat(3,1fr); gap:5mm;
    margin-block-end:4mm; break-inside:avoid;
  }
  .inv-block{ border:.5pt solid #b9b9b9; padding:2.4mm; border-radius:1mm }

  .inv-table{ width:100%; border-collapse:collapse }
  .inv-table th{ background:#eee; border-block-end:.75pt solid #000;
                 padding:1.5mm 2mm; text-align:start; font-weight:600 }
  .inv-table td{ padding:1.2mm 2mm; border-block-end:.25pt solid #ccc }
  .inv-table th.num-h, .inv-table td.num-c{ text-align:end }
  .inv-table thead{ display:table-header-group }

  /* The QR beside the totals, both anchored to the foot of the document.
     A QR that lands overleaf on its own is a second page nobody keeps. */
  .inv-foot{
    display:flex; gap:8mm; justify-content:space-between; align-items:flex-start;
    margin-block-start:5mm; break-inside:avoid;
  }
  .inv-qr{ text-align:center; flex:none }
  .inv-qr svg{ width:28mm; height:28mm; display:block }
  .inv-sum{ flex:1 }
  .inv-totals{ width:100%; border-collapse:collapse }
  .inv-totals td{ padding:1.2mm 2mm; border-block-end:.25pt solid #ccc }
  .inv-totals td:last-child{ text-align:end }
  .inv-totals .inv-grand td{
    border-block-start:1.5pt double #000; border-block-end:none;
    font-weight:900; font-size:11pt;
  }
  .inv-words{ margin-block-start:2mm; font-weight:600 }
  .inv-sub{ padding-inline-start:6mm }

  /* «مسودة» across the page. A draft return picked up as final is exactly
     how a wrong figure reaches a portal, and a footnote does not stop it. */
  .vatdoc{ position:relative }
  .vat-stamp{
    position:absolute; inset-block-start:40%; inset-inline:0;
    text-align:center; font-size:64pt; font-weight:900;
    color:#000; opacity:.10; transform:rotate(-24deg);
    letter-spacing:.1em; pointer-events:none;
  }
  .vat-flag{
    border:1.5pt solid #000; padding:2mm; margin-block-end:3mm;
    font-weight:600; break-inside:avoid;
  }
  .vatdoc tbody tr.row-group td{ background:#f4f4f4; font-weight:600 }
}


/* ==== packages/media/attach-field.css ==== */
/* ============================================================
   packages/media/attach-field.css — حقل المرفقات والعارض
   ============================================================
   لا ألوان خام: كل شيء من tokens.css، فتغيير اللوحة يصل إلى هنا.
   الاتجاه RTL موروث من <html dir="rtl"> — نستعمل الخصائص المنطقية
   (inline-start/end) فقط، فلا يوجد left/right في هذا الملف.
   ============================================================ */

.att-field{
  display:flex; flex-direction:column; gap:var(--s-sm);
  padding:var(--s-md); margin-block-start:var(--s-sm);
  border:1px solid var(--line); border-radius:var(--r-lg);
  background:var(--canvas-soft);
}
.att-head{ display:flex; align-items:center; justify-content:space-between; }

.att-bar{ display:flex; gap:var(--s-sm); flex-wrap:wrap; }
.att-btn{ display:inline-flex; align-items:center; gap:var(--s-xs); }
.att-ico{ font-size:16px; line-height:1; }

/* شريط المصغّرات — يمرّر أفقياً على الهاتف بدل أن يكسر النموذج.
 *
 * overscroll-behavior-x:contain هو ما يمنع «تسلسل التمرير»: بدونه، سحبة
 * تتجاوز آخر مصغّرة تنتقل إلى الحاوية الأعلى وتُزحلق النموذج كلّه جانباً.
 * و touch-action:pan-x وليس none — الحركة الأفقية لهذا الشريط، أما الرأسية
 * فتبقى للنموذج حتى يستمر التمرير لأعلى وأسفل فوق المصغّرات.
 */
.att-strip{
  display:flex; gap:var(--s-sm); overflow-x:auto; padding-block-end:var(--s-xs);
  scrollbar-width:thin; min-width:0;
  overscroll-behavior-x:contain; touch-action:pan-x;
}
.att-tile{ position:relative; flex:0 0 auto; width:96px; display:flex;
           flex-direction:column; gap:var(--s-xs); align-items:stretch; }

.att-thumb{
  width:96px; height:96px; border-radius:var(--r-md); cursor:pointer;
  background:var(--canvas) center/cover no-repeat;
  border:1px solid var(--line);
  display:flex; align-items:center; justify-content:center;
  color:var(--mute); font-size:24px;
}
.att-thumb:focus-visible{ outline:2px solid var(--primary); outline-offset:2px; }
/* المنتظر باهت: الفرق بين «موجود على الخادم» و«ما زال على الجهاز»
   يجب أن يُرى قبل قراءة أي نص. */
.att-thumb.pend{ opacity:.55; filter:grayscale(.4); }

.att-chip{
  display:block; width:100%; text-align:center;
  font-size:11px; font-weight:600; line-height:1.6;
  border-radius:var(--r-pill); border:0; padding:2px var(--s-xs);
  white-space:nowrap; overflow:hidden; text-overflow:ellipsis;
}
.att-chip.ok  { background:var(--primary-pale); color:var(--ink-deep); }
.att-chip.wait{ background:var(--accent-orange-bg); color:var(--accent-orange-ink); }
.att-chip.bad { background:color-mix(in srgb, var(--negative) 15%, var(--canvas));
                color:var(--negative-deep); cursor:pointer; }
.att-chip.bad:hover{ background:color-mix(in srgb, var(--negative) 25%, var(--canvas)); }

.att-del{
  position:absolute; inset-block-start:4px; inset-inline-end:4px;
  width:24px; height:24px; border-radius:var(--r-full); border:0;
  background:rgba(14,15,12,.62); color:#fff; font-size:12px; line-height:1;
  cursor:pointer; display:flex; align-items:center; justify-content:center;
}
.att-del:hover{ background:var(--negative); }

.att-note{ margin:0; }
.att-note.bad{ color:var(--negative-deep); font-weight:600; }

/* ---------------- العارض ----------------
   z-index أعلى من طبقة ‎#modals‎ لأنه يُفتح فوق نموذج مفتوح، وهو ملحق
   بـ <body> لا بـ #modals حتى لا يمحوه closeModal. */
.att-lightbox{
  position:fixed; inset:0; z-index:9000;
  background:rgba(14,15,12,.92);
  display:flex; flex-direction:column; align-items:center; justify-content:center;
  gap:var(--s-md); padding:var(--s-lg);
}
.att-stage{ flex:1; min-height:0; display:flex; align-items:center; justify-content:center;
            overflow:auto; width:100%; }
.att-full{ max-width:100%; max-height:100%; object-fit:contain;
           border-radius:var(--r-md); background:var(--canvas); }
.att-full.zoom{ max-width:none; max-height:none; width:auto; cursor:zoom-out; }
.att-cap{ color:var(--canvas-soft); font-size:13px; text-align:center; }
.att-close{
  position:absolute; inset-block-start:var(--s-lg); inset-inline-end:var(--s-lg);
  width:40px; height:40px; border-radius:var(--r-full); border:0;
  background:rgba(255,255,255,.14); color:#fff; font-size:18px; cursor:pointer;
}
.att-close:hover{ background:rgba(255,255,255,.26); }

@media (max-width:520px){
  .att-tile,.att-thumb{ width:76px; }
  .att-thumb{ height:76px; }
}
@media (prefers-reduced-motion:reduce){
  .att-thumb,.att-chip,.att-del{ transition:none; }
}


/* ==== packages/ui/camera.css ==== */
/* ============================================================
   packages/ui/camera.css — ورقة قارئ الرمز
   ============================================================
   بلا لون خام واحد: كل شيء من الرموز، وإلا سقط اختبار
   «no raw colour outside the token block».
   ============================================================ */

.cam-wrap{ display:flex; flex-direction:column; gap:var(--s-md) }

/* مربّع تماماً: الرمز مربّع، ومعاينة مستطيلة تجعل نصف ما يراه المستخدم
   خارج ما يُفحص فعلاً — وهو ما يجعل القارئ يبدو عشوائياً. */
.cam-stage{
  position:relative; width:100%; aspect-ratio:1 / 1;
  border-radius:var(--r-xl); overflow:hidden; background:var(--ink);
}
.cam-video{
  position:absolute; inset:0; width:100%; height:100%;
  object-fit:cover; display:block;
}

/* الإطار يرسم بالضبط القصاصة التي تُفحص — ٧٠٪ من الضلع الأقصر، وهي القيمة
   نفسها في CAM_ROI_FRAC. إطارٌ يعد بأكثر مما يُقرأ كذبة تجعل المستخدم يظن
   أن الرمز معطوب. الظلّ حول المربّع هو ما يقول «صوّب هنا» بلا كلمة. */
.cam-frame{ position:absolute; inset:0; display:grid; place-items:center;
  pointer-events:none }
.cam-frame > i{
  width:70%; aspect-ratio:1 / 1; border-radius:var(--r-lg);
  outline:9999px solid color-mix(in srgb, var(--ink) 55%, transparent);
  box-shadow:inset 0 0 0 3px var(--primary);
}

.cam-hint{ min-height:calc(var(--t-sm-lh) * 2); text-align:center; margin:0 }

.cam-torch{ flex:none; min-height:var(--touch) }
.cam-torch.on{ background:var(--primary); color:var(--on-primary) }

@media (prefers-reduced-motion:reduce){
  .cam-frame > i{ transition:none }
}


/* ==== packages/ui/gm.css ==== */
/* ============================================================
   packages/ui/gm.css — لوحة المدير العام
   ============================================================
   يعقوب's landing. Tokens only — every colour, radius, space and duration
   comes from tokens.css; the build test rejects a raw hex outside the token
   block, and DESIGN-product.md §4 rule 1 says why.

   The house rules, as they land here:
     · brand green is an ACTION — a link's hover ring, a decision button —
       never a figure, a chart or a status;
     · cyan is NEWS — the live dot, a figure that just moved, a new event;
     · warning and negative mean what they mean everywhere else;
     · 900 for the title and the figures, 600 for everything else;
     · motion is transform, opacity and stroke-dashoffset, and the global
       reduced-motion block (app.css) flattens all of it.

   Layout: twelve columns from 900 px, one column below; 44 px targets on
   a phone. The hero band is DESIGN-wise's hero-band-dark.
   ============================================================ */

.gm{ display:flex; flex-direction:column; gap:var(--s-lg); padding-block-end:var(--s-2xl); }
.gm-sr{
  position:absolute; width:1px; height:1px; margin:-1px; padding:0; overflow:hidden;
  clip:rect(0 0 0 0); white-space:nowrap; border:0;
}
.gm-link{
  font-size:var(--t-sm); font-weight:var(--w-strong); color:var(--ink); text-decoration:none;
  border-radius:var(--r-pill); padding:var(--s-xs) var(--s-sm); white-space:nowrap;
  transition:background var(--d-fast) var(--ease);
}
.gm-link:hover{ background:var(--primary-pale); }

/* ------------------------------------------------------ the hero band */

.gm-hero.phead{
  position:relative; overflow:hidden; isolation:isolate;
  background:var(--ink); color:var(--canvas);
  border-radius:var(--r-xl); padding:var(--s-xl);
  display:flex; flex-direction:column; gap:var(--s-lg);
}
/* A slow cyan glow in one corner — the only ornament, and it is news-blue. */
.gm-hero.phead::before{
  content:""; position:absolute; z-index:-1; inset-block-start:-40%; inset-inline-end:-10%;
  width:60%; aspect-ratio:1; border-radius:var(--r-full);
  background:radial-gradient(closest-side, color-mix(in srgb, var(--accent-cyan) 26%, transparent), transparent);
  animation:gm-glow 14s var(--ease) infinite alternate;
}
.gm-hero.phead::after{
  content:""; position:absolute; z-index:-1; inset-block-end:-55%; inset-inline-start:-8%;
  width:45%; aspect-ratio:1; border-radius:var(--r-full);
  background:radial-gradient(closest-side, color-mix(in srgb, var(--accent-orange) 14%, transparent), transparent);
}
@keyframes gm-glow{ from{ transform:translate(0, 0) scale(1); } to{ transform:translate(-8%, 10%) scale(1.12); } }

.gm-hero-top{ display:flex; align-items:flex-end; justify-content:space-between; gap:var(--s-lg); flex-wrap:wrap; }
.gm-hero-title{ min-width:0; }
.gm-hero h1{
  color:var(--canvas); font-weight:var(--w-hero);
  font-size:clamp(28px, 4.2vw, var(--t-display)); line-height:1.1; letter-spacing:-.02em;
}
.gm-eyebrow{
  margin:0 0 var(--s-xs); font-size:var(--t-sm); line-height:var(--t-sm-lh); font-weight:var(--w-strong);
  color:color-mix(in srgb, var(--canvas) 62%, var(--ink));
}
.gm-hero-acts{ display:flex; align-items:center; gap:var(--s-sm); flex-wrap:wrap; }
.gm-hero-btns{ display:inline-flex; gap:var(--s-xs); }
.gm-hero-btn{
  background:color-mix(in srgb, var(--canvas) 12%, var(--ink)); color:var(--canvas);
  box-shadow:inset 0 0 0 1px color-mix(in srgb, var(--canvas) 18%, var(--ink));
}
.gm-hero-btn:hover{ background:color-mix(in srgb, var(--canvas) 22%, var(--ink)); }

.gm-hero .seg{ background:color-mix(in srgb, var(--canvas) 10%, var(--ink)); }
.gm-hero .seg button{ color:color-mix(in srgb, var(--canvas) 78%, var(--ink)); }
.gm-hero .seg button.on{ background:var(--canvas); color:var(--ink); }

/* «● حيّ» — the dot breathes cyan while the pulse answers. */
.gm-live{
  display:inline-flex; align-items:center; gap:var(--s-sm); min-height:40px;
  padding:var(--s-xs) var(--s-md); border-radius:var(--r-pill);
  background:color-mix(in srgb, var(--canvas) 10%, var(--ink));
  font-size:var(--t-sm); font-weight:var(--w-strong); white-space:nowrap;
}
.gm-live-dot{
  position:relative; width:10px; height:10px; border-radius:var(--r-full); flex:none;
  background:var(--mute);
}
.gm-live.on .gm-live-dot{ background:var(--accent-cyan); }
.gm-live.on .gm-live-dot::after{
  content:""; position:absolute; inset:0; border-radius:inherit; background:var(--accent-cyan);
  animation:gm-ping 1.8s var(--ease) infinite;
}
.gm-live.off .gm-live-dot{ background:var(--warning); }
.gm-live.wait .gm-live-dot{ background:color-mix(in srgb, var(--canvas) 45%, var(--ink)); }
@keyframes gm-ping{ 0%{ transform:scale(1); opacity:.7; } 80%,100%{ transform:scale(2.6); opacity:0; } }
.gm-live-txt{ color:var(--canvas); }
.gm-ago{ font-size:var(--t-cap); font-weight:var(--w-normal); color:color-mix(in srgb, var(--canvas) 60%, var(--ink)); }

/* The ticker: the newest event in one line, sliding in when it changes. */
.gm-ticker{
  display:flex; align-items:center; gap:var(--s-sm); min-width:0;
  padding-block-start:var(--s-md);
  border-block-start:1px solid color-mix(in srgb, var(--canvas) 14%, var(--ink));
  font-size:var(--t-sm); color:color-mix(in srgb, var(--canvas) 84%, var(--ink));
}
.gm-ticker-tag{
  flex:none; padding:var(--s-xxs) var(--s-sm); border-radius:var(--r-pill);
  background:var(--accent-cyan); color:var(--ink); font-size:var(--t-cap); font-weight:var(--w-strong);
}
.gm-ticker-txt{ min-width:0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
.gm-ticker.tick .gm-ticker-txt{ animation:gm-tick var(--d-slow) var(--ease) 1; }
@keyframes gm-tick{ from{ opacity:0; transform:translateY(8px); } to{ opacity:1; transform:none; } }

.gm-err{ margin:0; }

/* ------------------------------------------------------ systems strip */

.gm-systems{
  display:grid; gap:var(--s-sm);
  grid-template-columns:repeat(auto-fill, minmax(180px, 1fr));
}
.gm-sys{
  display:flex; align-items:center; gap:var(--s-sm);
  min-height:44px; padding:var(--s-sm) var(--s-md);
  background:var(--canvas); color:inherit; text-decoration:none;
  border:1.5px solid transparent; border-radius:var(--r-lg);
  box-shadow:var(--shadow-1);
  transition:box-shadow var(--d-fast) var(--ease), border-color var(--d-fast) var(--ease);
}
a.gm-sys:hover{ box-shadow:var(--shadow-2); border-color:var(--primary); }
a.gm-sys:focus-visible{ outline:2px solid var(--ink); outline-offset:2px; }
.gm-sys .app-icon{
  width:36px; height:36px; margin:0; flex:none; font-size:17px;
}
.gm-sys-txt{ display:flex; flex-direction:column; min-width:0; }
.gm-sys-name{
  font-weight:var(--w-hero); font-size:var(--t-sm); color:var(--ink-deep);
  white-space:nowrap; overflow:hidden; text-overflow:ellipsis;
}
.gm-sys-sub{
  font-size:var(--t-cap); color:var(--mute);
  white-space:nowrap; overflow:hidden; text-overflow:ellipsis;
}
.gm-sys.soon{ opacity:.6; box-shadow:none; border-color:var(--line); }

/* ------------------------------------------------------ KPIs */

.gm-kpis{
  display:grid; gap:var(--s-md);
  grid-template-columns:repeat(auto-fill, minmax(188px, 1fr));
}
.gm-kpi{
  position:relative; display:flex; flex-direction:column; gap:var(--s-xs); min-width:0;
  padding:var(--s-lg); border-radius:var(--r-xl); background:var(--canvas);
  color:var(--ink); text-decoration:none; border:1.5px solid transparent;
  transition:transform var(--d-fast) var(--ease), box-shadow var(--d-fast) var(--ease),
             border-color var(--d-fast) var(--ease), background var(--d-mid) var(--ease);
  animation:gm-rise var(--d-slow) var(--ease) both;
}
.gm-kpi:hover{ transform:translateY(-2px); box-shadow:var(--shadow-2); border-color:var(--primary); }
.gm-kpi:focus-visible{ outline:3px solid var(--positive); outline-offset:2px; }
.gm-kpi-lab{ font-size:var(--t-cap); line-height:var(--t-cap-lh); font-weight:var(--w-strong); color:var(--body); }
.gm-kpi-val{ display:flex; align-items:baseline; gap:var(--s-xs); min-width:0; }
.gm-kpi-val .num{
  font-size:clamp(24px, 2.6vw, 34px); line-height:1.1; font-weight:var(--w-hero); letter-spacing:-.025em;
  white-space:nowrap;
}
.gm-kpi-unit{ font-size:var(--t-sm); font-weight:var(--w-strong); color:var(--mute); }
.gm-kpi-sub{
  font-size:var(--t-cap); line-height:var(--t-cap-lh); color:var(--mute);
  overflow:hidden; text-overflow:ellipsis; white-space:nowrap;
}
.gm-kpi.feature{ background:var(--ink); color:var(--canvas); }
.gm-kpi.feature .gm-kpi-lab{ color:color-mix(in srgb, var(--canvas) 70%, var(--ink)); }
.gm-kpi.feature .gm-kpi-unit,
.gm-kpi.feature .gm-kpi-sub{ color:color-mix(in srgb, var(--canvas) 58%, var(--ink)); }
.gm-kpi.bad .gm-kpi-val .num{ color:var(--negative-deep); }
.gm-kpi.bad::before{
  content:""; position:absolute; inset-block-start:var(--s-md); inset-inline-end:var(--s-md);
  width:8px; height:8px; border-radius:var(--r-full); background:var(--negative);
}
.gm-kpi.loading .gm-kpi-val .num{
  color:transparent; border-radius:var(--r-sm); min-width:5ch;
  background:linear-gradient(90deg, var(--canvas-soft), color-mix(in srgb, var(--canvas-soft) 40%, var(--canvas)), var(--canvas-soft));
  background-size:200% 100%; animation:gm-shimmer 1.4s linear infinite;
}
.gm-kpi.feature.loading .gm-kpi-val .num{
  background:linear-gradient(90deg, color-mix(in srgb, var(--canvas) 10%, var(--ink)),
    color-mix(in srgb, var(--canvas) 22%, var(--ink)), color-mix(in srgb, var(--canvas) 10%, var(--ink)));
  background-size:200% 100%;
}
@keyframes gm-shimmer{ from{ background-position:200% 0; } to{ background-position:-200% 0; } }
@keyframes gm-rise{ from{ opacity:0; transform:translateY(10px); } to{ opacity:1; transform:none; } }
.gm-kpis > :nth-child(2){ animation-delay:30ms; }  .gm-kpis > :nth-child(3){ animation-delay:60ms; }
.gm-kpis > :nth-child(4){ animation-delay:90ms; }  .gm-kpis > :nth-child(5){ animation-delay:120ms; }
.gm-kpis > :nth-child(6){ animation-delay:150ms; } .gm-kpis > :nth-child(7){ animation-delay:180ms; }
.gm-kpis > :nth-child(8){ animation-delay:210ms; } .gm-kpis > :nth-child(9){ animation-delay:240ms; }
.gm-kpis > :nth-child(10){ animation-delay:270ms; }

/* ------------------------------------------------------ the grid */

.gm-grid{ display:grid; gap:var(--s-lg); grid-template-columns:repeat(12, minmax(0, 1fr)); }
.gm-grid > *{ grid-column:span 12; min-width:0; }
@media (min-width:900px){
  .gm-stage{ grid-column:span 8; }
  .gm-inbox{ grid-column:span 4; }
  .gm-orders{ grid-column:span 8; }
  .gm-alerts-card{ grid-column:span 4; }
  .gm-stock, .gm-fridge-card, .gm-money-card{ grid-column:span 4; }
}
.gm-card{ display:flex; flex-direction:column; gap:var(--s-md); min-width:0; }
.gm-card-h{ display:flex; align-items:flex-start; justify-content:space-between; gap:var(--s-sm); flex-wrap:wrap; }
.gm-card-h h2{ margin:0; }
.gm-card-h > .t-cap{ margin-inline-start:auto; align-self:center; }
.gm-stage-sub{ margin:var(--s-xxs) 0 0; }
.gm-empty{ padding:var(--s-2xl) var(--s-lg); color:var(--body); }

/* ------------------------------------------------------ the stage + plan */

.gm-stage.gm-focus{ box-shadow:0 0 0 3px var(--accent-cyan); transition:box-shadow var(--d-slow) var(--ease); }
.gm-stage-body{ position:relative; min-width:0; }
.gm-3d-host{ position:relative; width:100%; aspect-ratio:46 / 29; border-radius:var(--r-lg);
  overflow:hidden; box-shadow:inset 0 0 0 1px var(--line);
  background:linear-gradient(180deg, var(--canvas) 0%, color-mix(in srgb, var(--canvas-soft) 70%, var(--canvas)) 100%); }
/* The 3D view: the canvas fills the frame; labels, the tooltip anchor and
   the presets sit over it. Labels are HTML so the browser shapes the Arabic. */
.gm-3d-canvas{ position:absolute; inset:0; width:100%; height:100%; display:block; touch-action:none;
  cursor:grab; outline:none; }
.gm-3d-canvas:active{ cursor:grabbing; }
.gm-3d-canvas:focus-visible{ box-shadow:inset 0 0 0 3px var(--accent-cyan); }
.gm-3d-labels{ position:absolute; inset:0; pointer-events:none; overflow:hidden; }
.gm-3d-lab{ position:absolute; left:0; top:0; display:flex; flex-direction:column; align-items:center;
  gap:1px; padding:2px var(--s-sm); border-radius:var(--r-pill); white-space:nowrap; font-family:var(--font);
  background:color-mix(in srgb, var(--canvas) 84%, transparent); box-shadow:var(--shadow-1);
  color:var(--ink); line-height:1.25; will-change:transform; }
.gm-3d-lab b{ font-size:var(--t-cap); font-weight:var(--w-hero); }
.gm-3d-lab small{ font-size:11px; font-weight:var(--w-strong); color:var(--body); }
.gm-3d-lab.k-zone, .gm-3d-lab.k-corridor{ background:transparent; box-shadow:none; }
.gm-3d-lab.k-corridor b{ color:var(--mute); font-weight:var(--w-strong); }
.gm-3d-lab.k-fridge{ background:color-mix(in srgb, var(--canvas) 92%, transparent); }
.gm-3d-anchor{ position:absolute; width:1px; height:1px; pointer-events:none; }
.gm-3d-probe{ position:absolute; width:0; height:0; overflow:hidden; visibility:hidden; }
.gm-3d-tools{ position:absolute; inset-inline:var(--s-sm); bottom:var(--s-sm); display:flex; flex-wrap:wrap;
  align-items:center; justify-content:space-between; gap:var(--s-xs) var(--s-sm); pointer-events:none; }
.gm-3d-tools > *{ pointer-events:auto; }
.gm-3d-presets{ box-shadow:var(--shadow-1); }
.gm-3d-hint{ margin:0; padding:2px var(--s-sm); border-radius:var(--r-pill); color:var(--body);
  background:color-mix(in srgb, var(--canvas) 80%, transparent); }
.gm-plan{ display:flex; flex-direction:column; gap:var(--s-sm); min-width:0; }
.gm-plan-tools{ display:flex; gap:var(--s-sm); flex-wrap:wrap; justify-content:space-between; }
.gm-plan-frame{
  border-radius:var(--r-lg); overflow:hidden;
  background:color-mix(in srgb, var(--canvas-soft) 70%, var(--canvas));
  box-shadow:inset 0 0 0 1px var(--line);
}
.gm-plan-svg{ display:block; width:100%; height:auto; aspect-ratio:46 / 29; }
.gm-plan-foot{ display:flex; align-items:center; justify-content:space-between; gap:var(--s-sm); flex-wrap:wrap; }
.gm-plan-legend{ display:flex; flex-wrap:wrap; gap:var(--s-xs) var(--s-md); }
.gm-lg{ display:inline-flex; align-items:center; gap:6px; font-size:var(--t-cap); color:var(--body); }
.gm-plan-note{ margin:0; }

/* The plan's user units are metres; text sizes below are in metres too. */
.gm-pl-floor{ fill:var(--canvas-soft); }
.gm-pl-corridor{ fill:color-mix(in srgb, var(--canvas-soft) 55%, var(--canvas)); stroke:var(--line); stroke-width:1;
  vector-effect:non-scaling-stroke; stroke-dasharray:6 5; }
.gm-pl-room{ fill:var(--canvas); stroke:var(--line); stroke-width:1; vector-effect:non-scaling-stroke; }
.gm-pl-fridge.mapped .gm-pl-room{ fill:color-mix(in srgb, var(--accent-cyan) 5%, var(--canvas)); }
.gm-pl-area.door .gm-pl-room{ fill:color-mix(in srgb, var(--warning) 7%, var(--canvas)); }
.gm-pl-area.bay .gm-pl-room{ fill:color-mix(in srgb, var(--accent-orange) 10%, var(--canvas)); }
.gm-pl-doorway{ fill:var(--ink); }
.gm-pl-aisle{ fill:color-mix(in srgb, var(--canvas-soft) 70%, var(--canvas)); }
.gm-pl-zone-bg{ fill:var(--canvas); stroke:var(--line); stroke-width:1; vector-effect:non-scaling-stroke; }
.gm-pl-zone.busy .gm-pl-zone-bg{ fill:color-mix(in srgb, var(--accent-cyan) 7%, var(--canvas)); }
.gm-plan text{ font-family:var(--font); fill:var(--ink); pointer-events:none; }
.gm-pl-title{ font-size:.78px; font-weight:var(--w-hero); }
.gm-pl-sub{ font-size:.5px; font-weight:var(--w-strong); fill:var(--mute) !important; }
.gm-pl-side{ font-size:.45px; font-weight:var(--w-strong); fill:var(--mute) !important; }
.gm-pl-zone-lab{ font-size:.6px; font-weight:var(--w-strong); fill:var(--mute) !important; letter-spacing:.2px; }
.gm-pl-warn{ font-size:.55px; font-weight:var(--w-strong); fill:var(--warning-deep) !important; }
.gm-pl-cell-lab{ font-size:.42px; font-weight:var(--w-strong); fill:var(--canvas) !important; }
.gm-pl-stack-lab{ font-size:.6px; font-weight:var(--w-hero); fill:var(--canvas) !important; }
.gm-pl-stack-sub{ font-size:.42px; font-weight:var(--w-strong); fill:var(--canvas) !important; }

.gm-cell{ cursor:pointer; outline:none; }
.gm-pl-box{ stroke:color-mix(in srgb, var(--ink) 22%, transparent); stroke-width:1; vector-effect:non-scaling-stroke;
  transition:stroke var(--d-fast) var(--ease), stroke-width var(--d-fast) var(--ease), opacity var(--d-fast) var(--ease); }
.gm-cell.s-free .gm-pl-box{ stroke:color-mix(in srgb, var(--ink) 10%, transparent); }
.gm-cell.s-blocked .gm-pl-box{ stroke-dasharray:3 2; }
.gm-cell.partial .gm-pl-box{ stroke-dasharray:4 2; stroke:var(--ink); }
.gm-cell.flag .gm-pl-box{ stroke:var(--warning-deep); stroke-width:2.5; }
.gm-cell.late .gm-pl-box{ stroke:var(--negative); stroke-width:2.5; }
.gm-cell:hover .gm-pl-box, .gm-cell.hot .gm-pl-box{ stroke:var(--ink); stroke-width:2.5; }
.gm-cell:focus-visible .gm-pl-box, .gm-cell:focus-visible .gm-pl-truck{ stroke:var(--positive); stroke-width:3.5; }
.gm-pl-truck{ fill:var(--ink); stroke:var(--ink); stroke-width:1; vector-effect:non-scaling-stroke; }
.gm-pl-cab{ fill:color-mix(in srgb, var(--ink) 70%, var(--canvas)); }
.gm-cell.k-truck.s-draft .gm-pl-truck{ fill:color-mix(in srgb, var(--ink) 45%, var(--canvas)); }
.gm-cell.k-truck:hover .gm-pl-truck{ stroke:var(--primary); stroke-width:3; }

/* ------------------------------------------------------ the flow */

.gm-flow-key{ display:inline-flex; align-items:center; gap:var(--s-xs); }
.gm-key-dot{ width:8px; height:8px; border-radius:var(--r-full); background:var(--accent-cyan); display:inline-block; }
.gm-flow-scroll{ overflow-x:auto; overscroll-behavior-x:contain; margin-inline:calc(var(--s-sm) * -1); }
.gm-flow-svg{ display:block; width:100%; min-width:760px; height:auto; }
.gm-flow-svg text{ font-family:var(--font); pointer-events:none; }
.gm-fnode{ cursor:pointer; outline:none; }
.gm-fn-ring{ fill:var(--canvas); stroke:var(--line); stroke-width:1.5; transition:stroke var(--d-fast) var(--ease); }
.gm-fn-core{ fill:var(--accent-cyan-bg); stroke:var(--accent-cyan-ink); stroke-width:1.5;
  transition:r var(--d-slow) var(--ease); }
.gm-fnode.empty .gm-fn-core{ fill:var(--canvas-soft); stroke:var(--mute); }
.gm-fnode.warn .gm-fn-ring{ stroke:var(--warning-deep); stroke-width:2.5; }
.gm-fnode:hover .gm-fn-ring{ stroke:var(--primary); stroke-width:3; }
.gm-fnode:focus-visible .gm-fn-ring{ stroke:var(--positive); stroke-width:4; }
.gm-fn-val{ font-size:13px; font-weight:var(--w-hero); fill:var(--ink); }
.gm-fn-lab{ font-size:14px; font-weight:var(--w-strong); fill:var(--ink); }
.gm-fn-sub{ font-size:12px; fill:var(--mute); }
.gm-fnode.blip .gm-fn-ring{ animation:gm-ring 1.2s var(--ease) 1; transform-box:fill-box; transform-origin:center; }
.gm-fnode.blip-still .gm-fn-ring{ fill:var(--accent-cyan-bg); }
@keyframes gm-ring{ 0%{ stroke:var(--accent-cyan); stroke-width:6; } 100%{ stroke:var(--line); stroke-width:1.5; } }
.gm-fl-track{ fill:none; stroke:var(--canvas-soft); stroke-linecap:round; }
.gm-fl-dots{ fill:none; stroke:var(--ink); stroke-linecap:round; stroke-dasharray:.1 22;
  animation:gm-flow 1s linear infinite; }
.gm-flink.idle .gm-fl-dots{ display:none; }
.gm-flink.idle .gm-fl-track{ stroke:var(--line); stroke-dasharray:4 7; stroke-width:2 !important; }
.gm-flink.still .gm-fl-dots{ animation:none; }
@keyframes gm-flow{ to{ stroke-dashoffset:-22.1; } }

/* ------------------------------------------------------ approvals */

.gm-ap-count{ align-self:center; }
.gm-ap-list, .gm-alerts, .gm-feed{ list-style:none; margin:0; padding:0; display:flex; flex-direction:column; }
.gm-ap-list{ gap:var(--s-xs); max-height:520px; overflow-y:auto; overscroll-behavior:contain; }
.gm-ap-row{
  display:grid; grid-template-columns:auto minmax(0, 1fr) auto; grid-template-areas:"ic txt amt" "ic acts acts";
  gap:var(--s-xs) var(--s-sm); align-items:center; padding:var(--s-sm);
  border-radius:var(--r-lg); background:var(--canvas-soft);
  animation:gm-rise var(--d-mid) var(--ease) both;
}
.gm-ap-ic{
  grid-area:ic; align-self:start; width:36px; height:36px; border-radius:var(--r-md);
  display:grid; place-items:center; background:var(--canvas); font-size:16px;
}
.gm-ap-txt{ grid-area:txt; display:flex; flex-direction:column; min-width:0; color:inherit; text-decoration:none; }
.gm-ap-txt:hover .gm-ap-title{ text-decoration:underline; text-underline-offset:3px; }
.gm-ap-title{ font-size:var(--t-sm); font-weight:var(--w-strong); overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
.gm-ap-sub{ display:flex; gap:var(--s-xs); align-items:center; min-width:0; font-size:var(--t-cap); color:var(--mute); }
.gm-ap-tag{ flex:none; padding:0 var(--s-xs); border-radius:var(--r-sm); background:var(--canvas); color:var(--body); }
.gm-ap-age{ flex:none; }
.gm-ap-row.old .gm-ap-age{ color:var(--warning-deep); font-weight:var(--w-strong); }
.gm-ap-amt{ grid-area:amt; font-size:var(--t-sm); font-weight:var(--w-strong); white-space:nowrap; }
.gm-ap-acts{ grid-area:acts; display:flex; gap:var(--s-xs); justify-content:flex-end; flex-wrap:wrap; }
.gm-ap-acts .btn{ min-height:36px; padding:var(--s-xs) var(--s-md); }
.gm-ap-empty, .gm-al-empty{
  display:flex; align-items:center; gap:var(--s-sm); padding:var(--s-lg);
  border-radius:var(--r-lg); background:var(--canvas-soft); color:var(--body); font-size:var(--t-sm);
}
.gm-ap-empty-ic{
  width:28px; height:28px; border-radius:var(--r-full); display:grid; place-items:center;
  background:var(--canvas); color:var(--positive-deep); font-weight:var(--w-hero);
}
.gm-ap-note{ width:100%; min-height:88px; }

/* ------------------------------------------------------ kanban + alerts */

.gm-kanban{
  display:grid; grid-auto-flow:column; grid-auto-columns:minmax(150px, 1fr); gap:var(--s-sm);
  overflow-x:auto; overscroll-behavior-x:contain; padding-block-end:var(--s-xxs);
}
.gm-kb-col{
  display:flex; flex-direction:column; gap:var(--s-xs); min-width:0;
  padding:var(--s-sm); border-radius:var(--r-lg); background:var(--canvas-soft);
  border-block-start:3px solid var(--mute);
}
.gm-kb-col.s-confirmed{ border-block-start-color:var(--ink); }
.gm-kb-col.s-picking, .gm-kb-col.s-packing{ border-block-start-color:var(--warning); }
.gm-kb-col.s-ready{ border-block-start-color:var(--positive); }
.gm-kb-col.s-shipped, .gm-kb-col.s-delivered{ border-block-start-color:var(--accent-cyan); }
.gm-kb-col.s-closed{ border-block-start-color:var(--line); }
.gm-kb-h{ display:flex; align-items:center; justify-content:space-between; gap:var(--s-xs); }
.gm-kb-lab{ font-size:var(--t-cap); font-weight:var(--w-strong); color:var(--body); white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
.gm-kb-n{ font-size:var(--t-h3); font-weight:var(--w-hero); }
.gm-kb-cards{ display:flex; flex-direction:column; gap:var(--s-xs); }
.gm-kb-card{
  display:flex; flex-direction:column; gap:2px; padding:var(--s-sm); min-height:44px;
  border-radius:var(--r-md); background:var(--canvas); color:inherit; text-decoration:none;
  border-inline-start:3px solid transparent;
  transition:box-shadow var(--d-fast) var(--ease), transform var(--d-fast) var(--ease);
}
.gm-kb-card:hover{ box-shadow:var(--shadow-2); transform:translateY(-1px); }
.gm-kb-card.late{ border-inline-start-color:var(--negative); }
.gm-kb-no{ font-size:var(--t-sm); font-weight:var(--w-strong); }
.gm-kb-cust{ font-size:var(--t-cap); color:var(--body); }
.gm-kb-meta{ display:flex; gap:var(--s-xs); flex-wrap:wrap; font-size:var(--t-cap); }
.gm-kb-bar{ height:4px; margin-block-start:var(--s-xxs); }
.gm-kb-bar > i{ background:var(--ink); }
.gm-kb-more{ color:var(--body); text-decoration:none; font-weight:var(--w-strong); padding:var(--s-xxs) var(--s-xs); }
.gm-kb-empty{ padding:var(--s-xs); }
.gm-late{ color:var(--negative-deep); font-weight:var(--w-strong); }
.gm-soon{ color:var(--warning-deep); font-weight:var(--w-strong); }

.gm-alerts{ gap:var(--s-xs); }
.gm-al a{
  display:flex; gap:var(--s-sm); align-items:flex-start; padding:var(--s-sm); min-height:44px;
  border-radius:var(--r-md); color:inherit; text-decoration:none;
  transition:background var(--d-fast) var(--ease);
}
.gm-al a:hover{ background:var(--canvas-soft); }
.gm-al-dot{ width:10px; height:10px; margin-block-start:5px; border-radius:var(--r-full); flex:none; background:var(--accent-cyan); }
.gm-al.sev-warn .gm-al-dot{ background:var(--warning); box-shadow:0 0 0 3px color-mix(in srgb, var(--warning) 25%, transparent); }
.gm-al.sev-urgent .gm-al-dot{ background:var(--negative); box-shadow:0 0 0 3px color-mix(in srgb, var(--negative) 20%, transparent); }
.gm-al-txt{ display:flex; flex-direction:column; min-width:0; }
.gm-al-title{ font-size:var(--t-sm); font-weight:var(--w-strong); }
.gm-al-body{ font-size:var(--t-cap); color:var(--mute); }

/* ------------------------------------------------------ stock, fridges, money */

.gm-donut .donut{ max-width:200px; }
.gm-fridges{ display:flex; flex-direction:column; gap:var(--s-sm); }
.gm-fr{
  display:flex; flex-direction:column; gap:var(--s-xs); padding:var(--s-sm); min-height:44px;
  border-radius:var(--r-md); color:inherit; text-decoration:none; transition:background var(--d-fast) var(--ease);
}
.gm-fr:hover{ background:var(--canvas-soft); }
.gm-fr-h{ display:flex; justify-content:space-between; align-items:baseline; gap:var(--s-sm); }
.gm-fr-name{ font-size:var(--t-sm); font-weight:var(--w-strong); }
.gm-fr-kg{ font-size:var(--t-sm); }
.gm-fr-bar > i{ background:var(--ink); }
.gm-fr-bar > i.full{ background:var(--negative); }

.gm-money{ display:flex; flex-direction:column; gap:var(--s-xs); }
.gm-money-hero{
  display:flex; flex-direction:column; gap:var(--s-xxs); padding:var(--s-md);
  border-radius:var(--r-lg); background:var(--canvas-soft); margin-block-end:var(--s-xs);
}
.gm-money-big .num{ font-size:clamp(24px, 2.4vw, 32px); line-height:1.15; font-weight:var(--w-hero); letter-spacing:-.02em; }
.gm-money-big .mute{ font-size:var(--t-sm); }
.gm-money .kv.warn .t-sm-b{ color:var(--warning-deep); }
.gm-ccy{ display:inline-flex; gap:var(--s-sm); flex-wrap:wrap; justify-content:flex-end; }
.gm-boxes{ display:flex; flex-direction:column; gap:var(--s-xxs); margin-block-start:var(--s-sm); }
.gm-boxes-h{ margin-block-end:var(--s-xxs); }
.gm-box{ display:flex; justify-content:space-between; gap:var(--s-sm); font-size:var(--t-sm); padding-block:var(--s-xxs); }
.gm-box-name{ color:var(--body); }

/* ------------------------------------------------------ feed */

.gm-feed{ gap:0; max-height:420px; overflow-y:auto; overscroll-behavior:contain; }
.gm-ev{
  display:grid; grid-template-columns:auto minmax(0, 1fr) auto; gap:var(--s-sm); align-items:baseline;
  padding:var(--s-sm) var(--s-md); border-inline-start:3px solid transparent;
  font-size:var(--t-sm);
}
.gm-ev + .gm-ev{ border-block-start:1px solid var(--canvas-soft); }
.gm-ev-at{ font-size:var(--t-cap); color:var(--mute); }
.gm-ev-txt{ color:var(--ink); text-decoration:none; min-width:0; }
a.gm-ev-txt:hover{ text-decoration:underline; text-underline-offset:3px; }
.gm-ev-who{ font-size:var(--t-cap); font-weight:var(--w-strong); color:var(--body); white-space:nowrap; }
.gm-ev.gm-new{
  border-inline-start-color:var(--accent-cyan);
  animation:gm-slide var(--d-slow) var(--ease) 1, gm-new-bg 4s var(--ease) 1;
}
@keyframes gm-slide{ from{ opacity:0; transform:translateX(24px); } to{ opacity:1; transform:none; } }
@keyframes gm-new-bg{ from{ background:var(--accent-cyan-bg); } to{ background:transparent; } }
.gm-ev.empty{ display:block; padding:var(--s-lg); }

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

@media (max-width:899px){
  .gm-hero.phead{ padding:var(--s-lg); border-radius:var(--r-xl); }
  .gm-hero-acts{ width:100%; }
  .gm-hero-btns{ margin-inline-start:auto; }
  .gm-hero .seg button, .gm-live, .gm-hero-btn{ min-height:var(--touch); }
  .gm-systems{
    grid-template-columns:none; grid-auto-flow:column; grid-auto-columns:minmax(150px, 1fr);
    overflow-x:auto; overscroll-behavior-x:contain; padding-block-end:var(--s-xxs);
  }
  .gm-kpis{ grid-template-columns:repeat(2, minmax(0, 1fr)); gap:var(--s-sm); }
  .gm-kpi{ padding:var(--s-md); min-height:var(--touch); }
  .gm-plan-tools .seg, .gm-3d-presets{ max-width:100%; overflow-x:auto; }
  .gm-3d-presets button{ min-height:var(--touch); }
  .gm-3d-hint{ display:none; }
  .gm-3d-host{ aspect-ratio:1 / 1; }
  .gm-plan-tools .seg button, .gm-link, .gm-kb-more{ min-height:var(--touch); display:inline-flex; align-items:center; }
  .gm-ap-acts .btn{ min-height:var(--touch); }
  .gm-ev{ min-height:var(--touch); }
  .gm-kanban{ grid-auto-columns:minmax(160px, 70%); scroll-snap-type:x mandatory; }
  .gm-kb-col{ scroll-snap-align:start; }
}

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

@media (prefers-reduced-motion:reduce){
  .gm-hero.phead::before, .gm-live.on .gm-live-dot::after, .gm-fl-dots,
  .gm-kpi, .gm-ap-row, .gm-ev.gm-new, .gm-ticker.tick .gm-ticker-txt,
  .gm-kpi.loading .gm-kpi-val .num{ animation:none !important; }
}

@media print{
  .gm-systems{ display:none; }
}


/* ==== packages/ui/env-ribbon.css ==== */
/* «تجريبي» — the mark a non-production build wears (apps/shell/env-ribbon.js).
   Tokens only. It sits above everything, takes no clicks, and never prints:
   a sheet that leaves the staging copy is marked by its data, not by this. */
.env-ribbon{
  position:fixed; z-index:100;
  inset-block-start:calc(var(--sat) + var(--s-xs));
  inset-inline-start:50%; transform:translateX(50%);
  padding:var(--s-xxs) var(--s-md);
  border-radius:var(--r-pill);
  background:var(--warning); color:var(--warning-content);
  font-size:var(--t-cap); line-height:var(--t-cap-lh); font-weight:var(--w-strong);
  box-shadow:var(--shadow-1);
  pointer-events:none; user-select:none; opacity:.92;
}
[dir="ltr"] .env-ribbon{ transform:translateX(-50%); }
@media print{ .env-ribbon{ display:none; } }
