/* ============================================================
   sourca — one stylesheet, no build step.

   The design language: warm paper canvas, white cards with soft
   shadows and generous radii, a dark rail for navigation, and one
   orange gradient reserved for the things that matter — the primary
   action and the money.

   Type is two families with distinct jobs. Space Grotesk sets
   headings, numerals and the wordmark; Plus Jakarta Sans does every
   other piece of interface text. Both are SELF-HOSTED, which is not
   a detail: Google is blocked in mainland China, the one country
   this app exists to be used in.

   One measurement worth keeping in view. White on the accent is
   3.07:1 at #FF5B2E and 4.04:1 at #E14A1F — under the 4.5:1 that
   normal-size text wants. It is fine on large bold button labels,
   which is where the design puts it. Do not start setting small
   white text on orange; use --accent-ink on a soft background
   instead, which is what --accent-soft-bg exists for.
   ============================================================ */

@import url("/fonts/fonts.css");

:root {
  /* ---- surfaces ---- */
  --bg: #edebe4;             /* the app canvas, warm paper */
  --surface-sunk: #f6f4ef;   /* the content well inside the shell */
  --surface: #ffffff;        /* cards */
  --ink: #15161c;            /* text, and the navigation rail */
  --ink-soft: #1f212b;       /* raised surfaces on the rail */

  --text: #15161c;
  --muted: rgba(21, 22, 28, .55);
  --faint: rgba(21, 22, 28, .45);
  --border: rgba(20, 21, 28, .07);
  --rule: rgba(20, 21, 28, .06);

  /* ---- accent ---- */
  --accent: #ff5b2e;
  --accent-deep: #e14a1f;
  --accent-grad: linear-gradient(135deg, #ff5b2e, #e14a1f);
  --accent-soft-bg: #ffe7da;
  --accent-ink: #e14a1f;     /* the accent, dark enough to read */

  /* ---- status ---- */
  --ok-bg: #def4e6;    --ok-ink: #1e9e5a;
  --danger-bg: #fbe0da; --danger-ink: #e0402a;
  --neutral-bg: #eeece6; --neutral-ink: #6b6d76;
  --warn-bg: #fdf0d5;  --warn-ink: #9a6207;

  /* Kept because templates and older rules still name them. */
  --brand: #ff5b2e;
  --primary: #e14a1f;
  --primary-dark: #c53f18;
  --accent-bg: #15161c;
  --accent-bg-dark: #000;
  --danger: #e0402a;
  --success: #1e9e5a;
  --warn: #9a6207;
  --hi: #ff5b2e;
  --hi-deep: #e14a1f;
  --hi-soft: #ffe7da;

  /* ---- shape ---- */
  --r-sm: 10px;   /* inputs */
  --r-btn: 11px;  /* buttons — NOT pills; only badges and avatars are */
  --r: 16px;      /* cards */
  --r-lg: 18px;   /* hero blocks */
  --r-xl: 22px;   /* page shell */
  --pill: 100px;  /* badges, avatars, the phone tab bar */
  --radius: var(--r);

  --shadow: 0 1px 2px rgba(20, 21, 28, .04), 0 8px 24px rgba(20, 21, 28, .05);
  --shadow-lift: 0 12px 32px rgba(20, 21, 28, .18);
  --shadow-shell: 0 40px 90px rgba(20, 21, 28, .16);

  --font: "Plus Jakarta Sans", system-ui, -apple-system, "PingFang SC",
          "Microsoft YaHei", sans-serif;
  --font-display: "Space Grotesk", "Plus Jakarta Sans", system-ui, sans-serif;

  --safe-bottom: env(safe-area-inset-bottom, 0px);
}

/* This design is light only, deliberately — it has no dark palette
   and half-applying one produced a UI that was neither. The scheme is
   declared so a phone set to dark does not have its own guesses
   applied to form controls and scrollbars. */
:root { color-scheme: light; }

* { box-sizing: border-box; }

body {
  margin: 0;
  font: 400 15px/1.55 var(--font);
  background: var(--bg);
  color: var(--text);
  -webkit-text-size-adjust: 100%;
  -webkit-font-smoothing: antialiased;
}

h1, h2, h3, .display { font-family: var(--font-display); letter-spacing: -.01em; }

a { color: var(--accent-ink); text-decoration: none; }
a:hover { color: var(--accent); }

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

   Desktop is a dark navigation rail beside a sunken content well,
   the pair floating on the paper canvas as one rounded card.
   Below 900px the rail is replaced by a floating bottom tab bar,
   because a sidebar on a phone is a drawer nobody opens.
   --------------------------------------------------------------- */

.shell {
  display: flex;
  width: min(1480px, 96vw);
  margin: 20px auto 0;
  background: var(--surface-sunk);
  border: 1px solid var(--border);
  border-radius: var(--r-xl);
  box-shadow: var(--shadow-shell);
  overflow: hidden;
  min-height: calc(100vh - 40px);
}

.rail {
  width: 264px; flex: none;
  background: var(--ink);
  padding: 20px 16px;
  display: flex; flex-direction: column; gap: 4px;
}

.rail .brand {
  display: flex; align-items: center;
  padding: 8px 4px 20px;
  color: #fff; text-decoration: none;
}

/* The supplied horizontal lockup is ONE image — mark and wordmark
   together — so nothing here reproduces the type, and the two can
   never drift apart. Sized by HEIGHT and left to find its own width:
   the artwork's aspect is fixed, and constraining both would
   letterbox it.

   The rail takes the DARK variant, whose wordmark is white; the auth
   card takes the light one. */
.brand .lockup { height: 38px; width: auto; display: block; }

/* An uploaded mark replaces the lockup entirely and is never cropped
   — `contain`, the same rule the business card follows and for the
   same reason: a cropped logo is somebody's brand with a corner
   missing. */
.mark.custom { background: none; border-radius: 0; }
.mark img { width: 100%; height: 100%; object-fit: contain; display: block; }

/* Whose data this is. A multi-org install must never leave that
   ambiguous, so it sits directly under the wordmark. */
.orgchip {
  display: flex; align-items: center; gap: 10px;
  padding: 10px 12px; margin-bottom: 14px;
  border-radius: 12px;
  background: rgba(255, 255, 255, .06);
  color: #fff; text-decoration: none;
}
.orgchip .initials {
  width: 30px; height: 30px; flex: none;
  border-radius: 9px;
  background: var(--accent-soft-bg); color: var(--accent-ink);
  font: 700 12px var(--font-display);
  display: flex; align-items: center; justify-content: center;
}
.orgchip .who { min-width: 0; }
.orgchip .name {
  font: 600 13px var(--font); color: #fff;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.orgchip .role { font: 500 11px var(--font); color: rgba(255, 255, 255, .45); }

/* ---- the business switcher ------------------------------------ */

.orgswitch { margin-bottom: 14px; }
.orgswitch > summary { list-style: none; cursor: pointer; margin-bottom: 0; }
.orgswitch > summary::-webkit-details-marker { display: none; }
.orgswitch .orgchip { margin-bottom: 0; }
.orgswitch .who { flex: 1; }

.orgmenu {
  margin-top: 6px; padding: 6px;
  background: var(--ink-soft);
  border-radius: 12px;
  box-shadow: 0 20px 40px rgba(0, 0, 0, .35);
}
.orgmenu form { margin: 0; }
.orgmenu button, .orgmenu .add {
  display: flex; align-items: center; gap: 10px; width: 100%;
  padding: 9px 10px; border: 0; border-radius: 9px;
  background: none; cursor: pointer; text-align: left;
  font: 600 13px var(--font); color: #fff; text-decoration: none;
}
.orgmenu button:hover, .orgmenu .add:hover { background: rgba(255, 255, 255, .08); }
.orgmenu button.on { background: rgba(255, 91, 46, .16); }
.orgmenu .ini {
  width: 24px; height: 24px; flex: none; border-radius: 7px;
  background: rgba(255, 255, 255, .1); color: #fff;
  font: 700 11px var(--font-display);
  display: flex; align-items: center; justify-content: center;
}
.orgmenu .add {
  color: var(--accent);
  border-top: 1px solid rgba(255, 255, 255, .08);
  margin-top: 6px; padding-top: 12px; border-radius: 0;
}

.rail nav { display: flex; flex-direction: column; gap: 4px; }
.rail nav a {
  display: flex; align-items: center; gap: 12px;
  padding: 10px 14px; border-radius: 12px;
  font: 500 14px var(--font);
  color: rgba(255, 255, 255, .56);
  text-decoration: none;
}
.rail nav a:hover { color: #fff; background: rgba(255, 255, 255, .05); }
.rail nav a.active {
  background: rgba(255, 91, 46, .16);
  color: #fff; font-weight: 600;
}
.rail nav a .ico { display: inline-flex; line-height: 0; }
.rail .icosvg { width: 18px; height: 18px; }   /* the handoff's size */

.rail .divider {
  margin: 14px 4px 6px; padding-top: 12px;
  border-top: 1px solid rgba(255, 255, 255, .08);
  font: 700 10px var(--font-display);
  letter-spacing: .12em; text-transform: uppercase;
  color: rgba(255, 255, 255, .32);
}
.rail .grow { flex: 1; }

.railout {
  display: flex; align-items: center; gap: 10px;
  width: 100%; padding: 10px 14px; border-radius: 12px;
  background: transparent; border: 0; cursor: pointer;
  font: 600 13px var(--font); color: rgba(255, 255, 255, .5);
  text-align: left;
}
.railout:hover { color: #fff; background: rgba(255, 255, 255, .05); }

.col { flex: 1; min-width: 0; display: flex; flex-direction: column; }

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

.topbar {
  height: 64px; flex: none;
  display: flex; align-items: center; gap: 16px;
  padding: 0 28px;
  border-bottom: 1px solid rgba(20, 21, 28, .08);
  background: var(--surface-sunk);
  position: sticky; top: 0; z-index: 30;
}
.topbar .spacer { flex: 1; }
.topbar a { color: var(--text); text-decoration: none; }
/* Both of these are the phone header. On desktop the rail already
   shows the wordmark and the business, and repeating them in the bar
   read as a duplicate rather than a heading. */
.topbar .brand, .topbar .orgchip { display: none; }

.topsearch {
  flex: 1; max-width: 340px;
  display: flex; align-items: center; gap: 10px;
  background: var(--surface); border: 1px solid rgba(20, 21, 28, .1);
  border-radius: var(--r-btn); padding: 9px 14px;
  font: 500 13px var(--font); color: var(--faint);
  text-decoration: none;
}
.topsearch:hover { color: var(--muted); }
.topsearch svg { flex: none; }

.topdiv { width: 1px; height: 26px; background: rgba(20, 21, 28, .1); flex: none; }

.topuser { display: flex; align-items: center; gap: 10px; }
.topuser .avatar {
  width: 34px; height: 34px; border-radius: var(--pill);
  background: var(--ink); color: #fff;
  font: 700 12px var(--font-display);
  display: flex; align-items: center; justify-content: center; flex: none;
}
.topuser .nm { font: 700 13px var(--font); }
.topuser .out {
  background: none; border: 0; padding: 0; cursor: pointer;
  font: 500 12px var(--font); color: var(--faint);
}
.topuser .out:hover { color: var(--accent-ink); }

/* The design puts a notification bell here. This app has no
   notifications and a dead bell is worse than none — but it does have
   something that belongs exactly here and matters far more in a
   market: whether your captures have actually reached the server. */
.syncchip {
  display: inline-flex; align-items: center; gap: 7px;
  padding: 8px 13px; border-radius: var(--r-btn);
  font: 600 12px var(--font);
  background: var(--ok-bg); color: var(--ok-ink);
  white-space: nowrap; flex: none;
}
.syncchip .dot { width: 7px; height: 7px; border-radius: 50%; background: currentColor; }
.syncchip[data-state="offline"] { background: var(--warn-bg); color: var(--warn-ink); }
.syncchip[data-state="pending"] { background: var(--accent-soft-bg); color: var(--accent-ink); }
.syncchip[data-state="pending"] .dot { animation: pulse 1.4s infinite; }
.syncchip[data-state="syncing"] { background: var(--accent-soft-bg); color: var(--accent-ink); }
.syncchip[data-state="syncing"] .dot { animation: pulse .8s infinite; }
.syncchip[data-state="error"] { background: var(--danger-bg); color: var(--danger-ink); }

@keyframes pulse { 50% { opacity: .3; } }

.scopechip {
  display: inline-flex; align-items: center;
  padding: 7px 12px; border-radius: var(--r-btn);
  font: 600 12px var(--font); white-space: nowrap;
  background: var(--accent-soft-bg); color: var(--accent-ink) !important;
  text-decoration: none;
}
@media (max-width: 720px) { .scopechip { display: none; } }

/* ---- the rail's footer block ----------------------------------- */

.railmeter {
  margin-top: auto;
  padding: 14px 12px;
  background: rgba(255, 255, 255, .06);
  border-radius: var(--r);
}
.railmeter .cap {
  font: 600 12px var(--font); color: #fff; margin-bottom: 8px;
}
.railmeter .bar {
  height: 6px; border-radius: var(--pill);
  background: rgba(255, 255, 255, .12);
  overflow: hidden; margin-bottom: 10px;
}
.railmeter .bar span {
  display: block; height: 100%;
  background: var(--accent);
  border-radius: var(--pill);
}
/* Over the allowance the bar is full and turns, because a bar that
   simply sits at 100% looks the same as one that is exactly on the
   line. Nothing is blocked either way — see src/limits.js. */
.railmeter .bar.over span { background: #ff8a5c; }
.railmeter a { font: 700 12px var(--font); color: #ff7a50; }
.railmeter a:hover { color: var(--accent); }

/* ---- the phone tab bar ----------------------------------------

   Floating and frosted, with the capture button raised out of it —
   capture is the one thing done a hundred times a day and it should
   never be a thing you go looking for.
   --------------------------------------------------------------- */

.tabbar { display: none; }

.sheet-backdrop {
  position: fixed; inset: 0; z-index: 59;
  background: rgba(20, 21, 28, .4);
}
.sheet {
  position: fixed; left: 0; right: 0; bottom: 0; z-index: 60;
  background: var(--surface);
  border-radius: 26px 26px 0 0;
  padding: 10px 20px calc(28px + var(--safe-bottom));
  box-shadow: 0 -10px 40px rgba(0, 0, 0, .2);
}
.sheet .grip {
  width: 36px; height: 4px; border-radius: var(--pill);
  background: rgba(20, 21, 28, .15);
  margin: 6px auto 14px;
}
.sheet a, .sheet button {
  display: flex; align-items: center; gap: 12px; width: 100%;
  padding: 13px 4px;
  border: 0; border-bottom: 1px solid var(--rule);
  background: none; cursor: pointer; text-align: left;
  font: 700 14px var(--font); color: var(--text); text-decoration: none;
}
.sheet a:last-child, .sheet button:last-child { border-bottom: 0; }
.sheet .danger { color: var(--danger-ink); }



@media (min-width: 900px) {
  .tabbar, .sheet, .sheet-backdrop { display: none !important; }
}

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

main {
  padding: 28px 24px;
  flex: 1;
  padding-bottom: calc(140px + var(--safe-bottom));
}
@media (max-width: 899px) { main { padding: 16px; padding-bottom: calc(150px + var(--safe-bottom)); } }

h1.page {
  font: 700 30px/1.15 var(--font-display);
  margin: 0 0 4px; letter-spacing: -.02em;
}
.sub { color: var(--muted); font-size: 14px; margin: 0 0 20px; }

.card {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--r);
  padding: 18px;
  margin-bottom: 14px;
  box-shadow: var(--shadow);
}
/* Uppercase, small, muted — the design labels a section rather than
   titling it, which keeps the content the loudest thing on the card. */
.card h2 {
  font: 700 13px var(--font);
  letter-spacing: .03em; text-transform: uppercase;
  color: rgba(21, 22, 28, .5);
  margin: 0 0 14px;
}

.lbl {
  font: 700 13px var(--font);
  letter-spacing: .03em; text-transform: uppercase;
  color: rgba(21, 22, 28, .5);
}

/* The install ask. Dismissible, but installing is what makes offline
   capture and persistent storage actually work. */
.installbar {
  display: flex; align-items: center; gap: 12px; flex-wrap: wrap;
  padding: 16px;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--r);
  box-shadow: var(--shadow);
  margin-bottom: 14px;
}
.installbar .small { color: var(--muted); margin-top: 3px; }
.installbar strong { font-size: 15px; font-family: var(--font-display); }

/* A card that folds away. The capture form uses one to keep the
   fields you fill in a market aisle apart from the ones that can
   wait until the hotel. */
.fold > summary {
  font: 700 15px var(--font-display);
  color: var(--text);
  padding: 12px 0; margin: -12px 0;   /* 44px of thumb target */
  cursor: pointer; list-style: none;
  display: flex; align-items: center; gap: 10px;
}

/* Safari draws its own triangle through ::-webkit-details-marker and
   ignores list-style, so it needs saying twice or two markers show. */
.fold > summary::-webkit-details-marker { display: none; }

.fold > summary::before {
  content: "+";
  font: 400 18px/1 var(--font);
  color: var(--accent-ink);
  background: var(--accent-soft-bg);
  border-radius: 9px;
  width: 26px; height: 26px; flex: 0 0 auto;
  display: flex; align-items: center; justify-content: center;
}
.fold[open] > summary::before { content: "\2212"; }   /* minus */
.fold[open] > summary { margin-bottom: 4px; }

/* The caption sits under the label rather than beside it: on a phone
   the two together wrap mid-phrase and read as one run-on line. */
.foldnote {
  display: block;
  font: 400 12px var(--font);
  letter-spacing: 0; text-transform: none;
  color: var(--muted); margin-top: 2px;
}
.foldbody { padding-top: 14px; }

.grid { display: grid; gap: 12px; }
.grid.stats { grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); }
/* ~4 across on a laptop, 2 on a phone — the design's proportions.
   170px gave six cramped columns on a wide screen. */
.grid.cards { grid-template-columns: repeat(auto-fill, minmax(232px, 1fr)); }
@media (max-width: 560px) { .grid.cards { grid-template-columns: repeat(2, 1fr); } }
.grid.two { grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); }
/* The dashboard's lower pair: the capture list gets the wider column,
   as the design has it. */
.grid.dash { grid-template-columns: 1.4fr 1fr; }
@media (max-width: 899px) { .grid.dash { grid-template-columns: 1fr; } }
.grid.stats { grid-template-columns: repeat(5, 1fr); }
@media (max-width: 1200px) { .grid.stats { grid-template-columns: repeat(3, 1fr); } }
@media (max-width: 640px)  { .grid.stats { grid-template-columns: repeat(2, 1fr); } }

.stat {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--r);
  padding: 18px;
}
.stat .label {
  font: 600 11px var(--font); color: var(--faint);
  text-transform: uppercase; letter-spacing: .04em;
  margin-bottom: 8px;
}
.stat .value {
  font: 700 26px/1.15 var(--font-display);
  color: var(--text); letter-spacing: -.02em;
  font-variant-numeric: tabular-nums;
  overflow-wrap: anywhere;
}
.stat .note { font: 500 11.5px var(--font); color: var(--faint); margin-top: 2px; }

/* One inverted card per row — the design gives the money figure its
   own weight without spending the accent on it. */
.stat.ink { background: var(--ink); border-color: var(--ink); color: #fff; }
.stat.ink .label, .stat.ink .note { color: rgba(255, 255, 255, .5); }
.stat.ink .value { color: #fff; font-size: 22px; }

/* ---- forms ---- */

/* The handoff's field label: uppercase, 11px, .04em, at 45% ink. It
   is a caption over the control, not a sentence beside it. */
label {
  display: block;
  font: 600 11px var(--font);
  letter-spacing: .04em; text-transform: uppercase;
  margin: 0 0 6px; color: var(--faint);
}
.field { margin-bottom: 16px; }

input, select, textarea {
  width: 100%; padding: 12px 14px; min-height: 46px;
  border: 1px solid rgba(20, 21, 28, .14); border-radius: var(--r-sm);
  background: var(--surface); color: var(--text);
  /* 16px on a PHONE and nowhere else.
     iOS zooms the page when you focus a field smaller than 16px, which
     is miserable one-handed in a market — that is why this project has
     always set it. But the zoom only happens on a touch device, and at
     16px every control reads a size larger than the handoff draws it.
     So the rule keeps its reason on the phone and the design's 13.5px
     on a desktop, where nothing zooms. */
  font-size: 16px;
  font-weight: 500; font-family: inherit;
  transition: border-color .12s ease, box-shadow .12s ease;
}

@media (min-width: 900px) {
  input, select, textarea { font-size: 13.5px; min-height: 44px; padding: 11px 13px; }
}

/* A <select> ignores nearly all of the above until its own appearance
   is turned off — border, radius and background all fall back to the
   platform control, which on macOS is a grey gradient with a
   double-arrow and looks nothing like the text field beside it.
   Turning it off means drawing the chevron ourselves.

   Not applied to a multiple or sized select: those are list boxes and
   the native control is the right one. */
select:not([multiple]):not([size]) {
  -webkit-appearance: none;
  -moz-appearance: none;
  appearance: none;
  padding-right: 40px;
  background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="%2315161c" stroke-opacity=".45" stroke-width="1.8"><path d="M6 9l6 6 6-6"/></svg>');
  background-repeat: no-repeat;
  background-position: right 12px center;
  background-size: 18px 18px;
}
/* Firefox keeps a focus-ring artefact on the text otherwise. */
select:-moz-focusring { color: transparent; text-shadow: 0 0 0 var(--text); }
input:focus, select:focus, textarea:focus {
  outline: none;
  border-color: var(--accent);
  box-shadow: 0 0 0 3px rgba(255, 91, 46, .16);
}
textarea { min-height: 84px; resize: vertical; }

.row { display: flex; gap: 12px; }
.row > * { flex: 1; min-width: 0; }

.btn {
  display: inline-flex; align-items: center; justify-content: center; gap: 7px;
  padding: 12px 20px; border-radius: var(--r-btn); border: 1px solid transparent;
  background: var(--ink); color: #fff;
  font: 700 13.5px var(--font);
  text-decoration: none; cursor: pointer; line-height: 1.2;
  /* 44px is the smallest comfortable target for a thumb, and this app
     is used one-handed while holding a phone in a crowded market. */
  min-height: 44px;
  transition: opacity .12s ease, transform .04s ease, background .12s ease;
}
.btn:hover { background: #24262f; color: #fff; }
.btn:active { transform: translateY(1px); }
.btn:disabled { opacity: .4; cursor: not-allowed; transform: none; }

/* White with a hairline. This is the everyday button in the design —
   "+ Capture item", the export actions, the filters. */
.btn.secondary {
  background: var(--surface); color: var(--text);
  border-color: rgba(20, 21, 28, .1);
}
.btn.secondary:hover { background: var(--surface-sunk); color: var(--text); }

.btn.ghost {
  background: transparent; color: var(--muted);
  border-color: rgba(20, 21, 28, .1);
}
.btn.ghost:hover { background: var(--surface); color: var(--text); }

.btn.danger {
  background: var(--danger-bg); color: var(--danger-ink);
  border-color: transparent;
}
.btn.danger:hover { background: #f7cec4; color: var(--danger-ink); }

/* The accent as a fill exists in exactly two places in this design:
   the money hero and the phone's raised capture button. Anywhere else
   it would stop meaning "this is the money" — and white on it is
   3.07:1, which is under AA for text this size. */
.btn.accent { background: var(--accent-grad); color: #fff; border-color: transparent; }
.btn.accent:hover { filter: brightness(1.06); background: var(--accent-grad); }

.btn.block { width: 100%; }
.btn.lg { padding: 15px 24px; font-size: 15px; min-height: 52px; }
.btn.sm { padding: 8px 14px; font-size: 12.5px; min-height: 36px; }

/* Keyboard users get a visible ring; mouse users do not. */
:focus-visible {
  outline: 3px solid var(--accent);
  outline-offset: 2px;
  border-radius: 6px;
}

.btnrow { display: flex; gap: 8px; flex-wrap: wrap; }

/* ---- tables ---- */

.tablewrap {
  overflow-x: auto; -webkit-overflow-scrolling: touch;
  border-radius: var(--r-sm);
  /* A fading edge is the only honest hint that a wide table on a
     phone scrolls sideways. */
  background:
    linear-gradient(to right, var(--surface) 30%, rgba(255, 255, 255, 0)) left / 24px 100% no-repeat,
    linear-gradient(to left,  var(--surface) 30%, rgba(255, 255, 255, 0)) right / 24px 100% no-repeat;
}
table { width: 100%; border-collapse: collapse; font-size: 14px; }
th, td {
  padding: 12px 14px; text-align: left;
  border-bottom: 1px solid var(--rule); white-space: nowrap;
}
th {
  position: sticky; top: 0; z-index: 1;
  background: var(--surface-sunk); color: var(--muted);
  font: 600 12px var(--font);
  text-transform: none; letter-spacing: 0;
}
td.num, th.num { text-align: right; font-variant-numeric: tabular-nums; }
tbody tr:hover { background: var(--surface-sunk); }
tbody tr:last-child td { border-bottom: 0; }

/* ---- product cards ---- */

.pcard {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--r);
  overflow: hidden; position: relative;
  display: flex; flex-direction: column;
  box-shadow: var(--shadow);
  transition: transform .12s ease, box-shadow .12s ease;
}
.pcard:hover { transform: translateY(-2px); box-shadow: 0 14px 30px rgba(20, 21, 28, .10); }
.pcard .thumb {
  aspect-ratio: 1; background: var(--surface-sunk) center/cover no-repeat;
  display: block;
}
.pcard .body {
  padding: 12px 13px 13px; flex: 1;
  display: flex; flex-direction: column; gap: 3px;
}
.pcard .code {
  font: 600 11px var(--font-display); color: var(--faint);
  font-variant-numeric: tabular-nums; letter-spacing: .02em;
}
.pcard .name {
  font: 600 14px/1.35 var(--font);
  display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical;
  overflow: hidden;
}
.pcard .shop {
  font-size: 12px; color: var(--muted);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.pcard .prices {
  margin-top: auto; padding-top: 10px;
  border-top: 1px solid var(--rule);
  display: flex; justify-content: space-between; align-items: baseline; gap: 6px;
  font-variant-numeric: tabular-nums;
}
.pcard .cny { font: 700 16px var(--font-display); letter-spacing: -.02em; }
.pcard .lkr { font: 700 12px var(--font); color: var(--accent-ink); }

/* The status pill sits ON the photo so the body keeps its lines for
   the name, which is what you actually read. */
.pcard .statuspill {
  position: absolute; top: 9px; right: 9px; z-index: 2;
  box-shadow: 0 2px 8px rgba(20, 21, 28, .16);
}

/* A bare checkbox vanishes against a photo, so it gets its own chip. */
.pcard .tick {
  position: absolute; top: 8px; left: 8px; z-index: 3;
  width: 26px; height: 26px; margin: 0;
  accent-color: var(--accent);
  border-radius: 8px;
  background: #fff; border: 1px solid var(--border);
  box-shadow: 0 2px 8px rgba(20, 21, 28, .16);
  cursor: pointer;
}
.pcard.selected { outline: 3px solid var(--accent); outline-offset: -3px; }
tr.selected { background: var(--accent-soft-bg) !important; }

/* The same control in the table view, where there is no photo to
   sit on but the target still has to be thumb-sized. */
td .tick { width: 22px; height: 22px; accent-color: var(--accent); cursor: pointer; }

/* ---- badges ---- */

.badge {
  display: inline-block; padding: 4px 11px; border-radius: var(--pill);
  font: 700 10.5px var(--font);
  text-transform: none; letter-spacing: 0;
  background: var(--neutral-bg); color: var(--neutral-ink); border: 0;
}
.badge.shortlisted { background: var(--accent-soft-bg); color: var(--accent-ink); }
.badge.ordered    { background: var(--ok-bg);      color: var(--ok-ink); }
.badge.rejected   { background: var(--danger-bg);  color: var(--danger-ink); }
.badge.draft      { background: var(--neutral-bg); color: var(--neutral-ink); }

/* ---- the bottom bars ------------------------------------------

   Both float above the content as their own rounded slab, matching
   the phone tab bar rather than spanning the full width as a black
   band. They live in the same corner of the screen, so `body.acting`
   (set by app.js whenever one is showing) hides the tab bar: two
   stacked bars eat a third of a phone screen, and while you are
   mid-form the save action is the only one that matters.
   --------------------------------------------------------------- */

.selbar, .savebar {
  position: fixed; z-index: 56;
  left: 12px; right: 12px; bottom: calc(12px + var(--safe-bottom));
  align-items: center; gap: 10px; flex-wrap: wrap;
  padding: 12px 14px;
  border-radius: var(--r-lg);
  background: var(--ink);
  box-shadow: var(--shadow-lift);
  border: 0;
}
.selbar .count, .savebar .hint { color: rgba(255, 255, 255, .68); }
.selbar .btn.ghost { color: rgba(255, 255, 255, .7); border-color: rgba(255, 255, 255, .18); }
.selbar .btn.ghost:hover { color: #fff; background: rgba(255, 255, 255, .08); }
.selbar { display: none; }
.selbar.show { display: flex; }
.selbar .count { font: 600 14px var(--font); color: #fff; }
.selbar .count .n { color: var(--accent); font-family: var(--font-display); font-weight: 700; }

/* The capture form is long; the save action must never be a scroll
   away when you are standing at a stall. */
.savebar { display: flex; }
.savebar .btn { flex: 1 1 auto; }
.savebar .hint {
  flex: 1 1 100%; order: -1;
  font: 500 11.5px var(--font);
  letter-spacing: 0; text-transform: none;
}

/* Set by app.js when a save or selection bar is on screen. */
body.acting .tabbar { display: none; }

@media (min-width: 900px) {
  .selbar, .savebar {
    left: 50%; right: auto; transform: translateX(-50%);
    bottom: 20px; width: min(760px, calc(100% - 48px));
  }
}

/* ---- capture screen ---- */

.shots { display: grid; grid-template-columns: repeat(auto-fill, minmax(88px, 1fr)); gap: 10px; }
.shot {
  position: relative; aspect-ratio: 1; overflow: hidden;
  border-radius: 12px; border: 1px solid var(--border);
  background: var(--surface-sunk);
}
.shot img { width: 100%; height: 100%; object-fit: cover; display: block; }
.shot button {
  position: absolute; top: 6px; right: 6px;
  width: 24px; height: 24px; border-radius: 50%; border: 0;
  background: rgba(20, 21, 28, .6); color: #fff; font-size: 14px; line-height: 1; cursor: pointer;
}

/* The dropzone spans the whole card and keeps its height whether or
   not photos have been taken — the design leads this screen with a
   large, obvious target, and a small tile in a corner of a grid is
   not that. Thumbnails fill in on the row beneath it. */
.shotadd {
  grid-column: 1 / -1;
  aspect-ratio: auto; min-height: 168px;
  border: 2px dashed rgba(20, 21, 28, .18);
  border-radius: 14px;
  background: transparent; color: var(--faint);
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  gap: 8px; font: 600 13.5px var(--font); cursor: pointer;
  transition: border-color .12s ease, color .12s ease;
}
.shotadd:hover { border-color: var(--accent); color: var(--accent-ink); }

/* ---- the capture screen's two columns -------------------------- */

.capgrid { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; align-items: start; }
@media (max-width: 899px) { .capgrid { grid-template-columns: 1fr; gap: 0; } }
.capcol { min-width: 0; }

/* A price block sitting inside a card rather than leading a page. */
.pricebox.tight { padding: 18px; border-radius: 14px; }
.pricebox.tight .big { font-size: 30px; }
.pricebox.tight .split { margin-top: 14px; gap: 8px; }

.btn.pin { justify-content: center; }
.btn.pin svg { color: var(--accent); }

/* ---- active shop panel (capture screen) ---- */

.activeshop {
  display: flex; gap: 12px; align-items: center; flex-wrap: wrap;
  padding: 13px;
  border: 2px solid var(--ink);
  border-top: 6px solid var(--hi);
  background: var(--surface);
}
.activeshop .as-body { flex: 1 1 180px; min-width: 0; }
.activeshop .as-label {
  font-family: var(--font);
  font-size: 11px; font-weight: 700; text-transform: uppercase;
  letter-spacing: .08em; color: var(--muted);
}
.activeshop .as-name {
  font-size: 22px; font-weight: 800; line-height: 1.15;
  letter-spacing: -.025em; overflow-wrap: anywhere;
}
.activeshop .as-meta { font-size: 13px; color: var(--muted); }
.activeshop .as-count {
  font-size: 12px; font-weight: 800; color: var(--primary); margin-top: 4px;
}
.activeshop .as-actions {
  display: flex; gap: 8px; flex: 0 0 auto; flex-wrap: wrap;
}
.activeshop .as-actions .btn { padding: 8px 14px; font-size: 13px; }

/* ---- shop grouping (products screen) ---- */

.shopgroup { margin-bottom: 20px; }
.shopgroup > .shophead {
  display: flex; align-items: baseline; gap: 10px; flex-wrap: wrap;
  padding: 9px 13px; margin-bottom: 10px;
  background: var(--ink); color: #fff; border: 2px solid var(--ink);
}
.shopgroup > .shophead .meta { color: #a3a9b2; }
.shopgroup > .shophead a { color: var(--hi); }
.shopgroup > .shophead .name { font-size: 16px; font-weight: 800; letter-spacing: -.02em; }
.shopgroup > .shophead .meta { font-size: 12px; color: var(--muted); }
.shopgroup > .shophead .spacer { flex: 1; }

/* The money block — the one place the accent gradient carries a full
   figure. White on it measures 3.07:1 at #FF5B2E and 4.04:1 at
   #E14A1F: fine for the 42px figure and the bold labels, NOT fine for
   small text, which is why every value below sits in its own
   lightened cell rather than directly on the orange. */
.pricebox {
  background: var(--accent-grad);
  border-radius: var(--r-lg);
  padding: 26px 28px;
  color: #fff;
}
.pricebox .head {
  display: flex; align-items: flex-end; justify-content: space-between;
  flex-wrap: wrap; gap: 16px;
}
.pricebox .formula {
  font: 600 12px var(--font);
  color: rgba(255, 255, 255, .75);
  margin-bottom: 6px;
  font-variant-numeric: tabular-nums;
}
.pricebox .formula b, .pricebox .formula strong { color: #fff; font-weight: 700; }
.pricebox .big {
  font: 700 42px/1.05 var(--font-display);
  color: #fff; letter-spacing: -.02em;
  font-variant-numeric: tabular-nums;
}
.pricebox .split {
  display: grid; grid-template-columns: repeat(3, 1fr);
  gap: 12px; margin-top: 20px;
}
.pricebox .split div {
  background: rgba(255, 255, 255, .14);
  border-radius: 12px;
  padding: 12px 14px;
  font: 600 10.5px var(--font);
  letter-spacing: .03em; text-transform: uppercase;
  color: rgba(255, 255, 255, .7);
}
.pricebox .split strong {
  display: block;
  font: 700 16px var(--font-display);
  letter-spacing: -.01em; text-transform: none;
  color: #fff; font-variant-numeric: tabular-nums;
  margin-top: 4px;
}

/* The hero's own buttons: translucent, then solid white with the
   accent as its TEXT — which is how the design gets a readable
   second action without a second colour. */
.pricebox .btn {
  background: rgba(255, 255, 255, .18); color: #fff; border-color: transparent;
  min-height: 42px; padding: 12px 20px; font-size: 13.5px;
}
.pricebox .btn:hover { background: rgba(255, 255, 255, .28); color: #fff; }
.pricebox .btn.solid { background: #fff; color: var(--accent-deep); }
.pricebox .btn.solid:hover { background: #fff; color: var(--accent-deep); opacity: .92; }

@media (max-width: 560px) {
  .pricebox { padding: 20px; }
  .pricebox .big { font-size: 32px; }
  .pricebox .split { grid-template-columns: repeat(2, 1fr); }
  .pricebox .head { align-items: stretch; }
  .pricebox .head .btnrow { width: 100%; }
  .pricebox .head .btn { flex: 1; }
}

/* The greeting row: heading on the left, one action on the right. */
.pagehead {
  display: flex; align-items: center; justify-content: space-between;
  gap: 16px; flex-wrap: wrap; margin-bottom: 24px;
}
.pagehead .sub { margin: 6px 0 0; }
.pagehead h1.page { margin: 0; }

/* A card heading with an action beside it. */
.cardhead {
  display: flex; align-items: center; justify-content: space-between;
  gap: 12px; margin-bottom: 14px;
}
.cardhead h2 { margin: 0; }
.cardhead a { font: 700 12.5px var(--font); }

/* A plain list of rows inside a card — recently captured, markets,
   invoices. The design uses this shape everywhere instead of a table. */
.rows a, .rows .row-item {
  display: flex; align-items: center; gap: 12px;
  padding: 11px 0; border-bottom: 1px solid var(--rule);
  text-decoration: none; color: inherit;
}
.rows a:last-child, .rows .row-item:last-child { border-bottom: 0; }
.rows .thumb {
  width: 42px; height: 42px; flex: none; border-radius: 11px;
  background: var(--surface-sunk) center/cover no-repeat;
}
.rows .grow { flex: 1; min-width: 0; }
.rows .nm {
  display: block;
  font: 700 13.5px var(--font);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.rows .meta { display: block; font: 500 12px var(--font); color: var(--faint); }
.rows .money { text-align: right; font-variant-numeric: tabular-nums; }
.rows .money .cny { display: block; }
.rows .money .lkr { display: block; }
/* Shown on desktop, hidden on a phone. Used where the handoff's
   mobile frame drops a word the desktop line keeps. */
.deskonly { display: inline; }
.rows .money .cny { font: 700 13.5px var(--font-display); }
.rows .money .lkr { font: 700 12px var(--font); color: var(--accent-ink); }
.rows .count { font: 700 13px var(--font-display); color: var(--faint); }

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

.empty { text-align: center; padding: 48px 16px; color: var(--muted); }
.empty .big { font-size: 40px; margin-bottom: 8px; }
.muted { color: var(--muted); }
.small { font-size: 13px; }
.mono { font-variant-numeric: tabular-nums; font-family: ui-monospace, SFMono-Regular, Menlo, monospace; }
/* Tinted blocks, no border and no dark variant: this design is light
   only, and the dark overrides that used to live here fired on a
   dark-preference phone and put three dark patches in a light page. */
.alert {
  padding: 13px 16px; border-radius: var(--r-sm);
  margin-bottom: 16px; font-size: 14px; font-weight: 500; border: 0;
}
.alert.error { background: var(--danger-bg);      color: var(--danger-ink); }
.alert.ok    { background: var(--ok-bg);          color: var(--ok-ink); }
.alert.info  { background: var(--accent-soft-bg); color: var(--accent-ink); }

.filters { display: flex; gap: 10px; flex-wrap: wrap; align-items: flex-end; }

/* On a wide screen the handoff lays the filters out as one row:
   search, status, shop, sort, Filter, and the view switcher.

   Done with FLEX PROPORTIONS rather than a grid. The obvious version
   was `display: grid` on the form with `display: contents` on the
   <details> that collapses the secondary filters on a phone — but a
   <details> does not reliably hand its children to an ancestor grid
   that way: two of the six columns computed to 0px and the fields
   landed on three separate rows. Flex needs no such trick, and the
   proportions are the same. */
@media (min-width: 900px) {
  .filters.grid6 { flex-wrap: nowrap; align-items: flex-end; gap: 14px; }
  .filters.grid6 > .field.grow { flex: 1.6 1 0; min-width: 0; }
  .filters.grid6 .morefilters { flex: 3 1 0; min-width: 0; }
  .filters.grid6 .morefilters > .fields { flex-wrap: nowrap; gap: 14px; }
  .filters.grid6 .morefilters > .fields .field { flex: 1 1 0; min-width: 0; }
  .filters.grid6 > .field.auto { flex: 0 0 auto; }
  .filters.grid6 .field { margin: 0; }
}

.filters .field { margin: 0; flex: 1 1 150px; }
.filters .field.grow { flex: 2 1 240px; }
.filters .field.auto { flex: 0 0 auto; }

/* Secondary filters: collapsed on a phone, inline on a wide screen
   (app.js sets `open` above 760px). */
.morefilters { flex: 1 1 100%; }
.morefilters > summary {
  cursor: pointer; list-style: none;
  display: inline-flex; align-items: center; gap: 8px;
  padding: 10px 14px; min-height: 44px;
  border: 2px solid var(--border); border-radius: 0;
  font-size: 14px; font-weight: 700; color: var(--text);
  background: var(--surface);
}
.morefilters > summary::-webkit-details-marker { display: none; }
.morefilters > summary::after { content: '▾'; font-size: 11px; }
.morefilters[open] > summary::after { content: '▴'; }
.morefilters > .fields {
  display: flex; gap: 10px; flex-wrap: wrap; margin-top: 10px;
}
.morefilters > .fields .field { flex: 1 1 150px; margin: 0; }

@media (min-width: 760px) {
  /* Part of the single filter row, with no disclosure control. */
  .morefilters { flex: 3 1 460px; }
  .morefilters > summary { display: none; }
  .morefilters > .fields { margin-top: 0; }
}

@media (max-width: 759px) {
  /* Those labels are `&nbsp;` spacers that align buttons with the
     inputs beside them. Stacked on a phone there is nothing to align
     to, and they just burn a line each. */
  .filters .field.auto > label { display: none; }
  .filters .field.auto { flex: 0 1 auto; }
}

/* A view toggle is a set of related choices, so it reads as one
   control rather than three loose buttons. */
/* The handoff's view switcher: one hairline box, the active segment
   filled with the accent. It is the one place outside the money hero
   and the capture button where the accent is a fill — and it is
   carrying 13px bold white, which measures 3.07:1. Acceptable on a
   short bold label at button size; do not put a sentence on it. */
.segmented {
  display: inline-flex; overflow: hidden;
  border: 1px solid rgba(20, 21, 28, .14);
  border-radius: var(--r-sm);
  background: var(--surface);
}
.segmented a {
  padding: 11px 14px; font: 600 13px var(--font);
  color: var(--text); text-decoration: none; white-space: nowrap;
  border-left: 1px solid rgba(20, 21, 28, .1);
  min-height: 44px;
  display: inline-flex; align-items: center;
}
.segmented a:first-child { border-left: 0; }
.segmented a:hover { background: var(--surface-sunk); color: var(--text); }
.segmented a.on { background: var(--accent); color: #fff; font-weight: 700; }
.segmented a.on:hover { background: var(--accent); color: #fff; }

/* The floating button and the selection bar both live bottom-right;
   when something is ticked, the bar is the one that matters. */
body.has-selection .fab { display: none; }

/* The floating "+" is gone: on a phone the tab bar's raised capture
   button does that job in the same corner of the thumb's reach, and
   two capture affordances on one screen is one too many. On desktop
   the rail carries Capture. Templates still emit .fab, so it is
   hidden here rather than chased through every view. */
.fab { display: none; }

/* ---- login ---- */

.loginwrap {
  min-height: 100dvh; display: flex; align-items: center;
  justify-content: center; padding: 24px;
  background: var(--bg);
}
.loginwrap .card {
  width: 100%; max-width: 400px; margin: 0;
  border-radius: var(--r-lg); padding: 28px;
}
.loginwrap .logo {
  display: flex; align-items: center; justify-content: center;
  margin-bottom: 10px;
}
/* The light variant of the lockup: this card is white and the
   artwork's wordmark is black. */
.loginwrap .logo .lockup { height: 46px; width: auto; display: block; }

/* Google's brand guidance: white button, grey border, their mark. */
.btn.google {
  background: #fff; color: #1f1f1f; border-color: #dadce0;
  font-weight: 500; gap: 10px; box-shadow: none;
}
.btn.google:hover { background: #f7f8f8; border-color: #c9ccd1; color: #1f1f1f; filter: none; }
.btn.google svg { flex: 0 0 auto; }


.orline {
  display: flex; align-items: center; gap: 12px;
  margin: 16px 0; color: var(--muted); font-size: 12px;
  text-transform: uppercase; letter-spacing: .6px;
}
.orline::before, .orline::after {
  content: ''; flex: 1; height: 1px; background: var(--border);
}

/* ---- print ---- */

@media print {
  .topbar, .nav, .selbar, .fab, .noprint { display: none !important; }
  body { background: #fff; color: #000; font-size: 11pt; }
  main { padding: 0; max-width: none; }
  .card { box-shadow: none; border: 1px solid #999; break-inside: avoid; }
  .ordergroup { break-inside: avoid; page-break-inside: avoid; }
  a { color: #000; text-decoration: none; }
}

/* ---- business card -------------------------------------------
   The frame takes the CARD's shape, rather than the card being
   letterboxed into a fixed one.
   
   A fixed landscape box was the first attempt, so the page would not
   jump between shops. But a 54x90 card then fills a third of it and
   the writing is too small to read — and reading it is the entire
   reason the photo is kept. Filling the frame wins; the height is
   capped so a tall card cannot run away with the page.
   -------------------------------------------------------------- */
.cardframe {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  max-width: 100%;
  background: var(--bg);
  border: 2px solid var(--ink);
  overflow: hidden;
}

.cardframe img {
  display: block;
  /* Sized by height, with width following the card's own aspect, so
     landscape and portrait both fill the frame edge to edge. */
  max-height: 340px;
  max-width: 100%;
  width: auto;
  height: auto;
  object-fit: contain;
}

/* The empty state has no image to size it, so it needs its own box. */
.cardframe.isempty {
  display: flex;
  width: 100%;
  height: 120px;
}

.cardframe .empty {
  font-family: var(--font);
  font-size: 12px;
  font-weight: 700;
  letter-spacing: .08em;
  text-transform: uppercase;
  color: var(--muted);
}

.cardrow {
  display: flex;
  gap: 10px;
  align-items: center;
  flex-wrap: wrap;
  margin-top: 10px;
}

/* The frame doubles as the link to the full-size card. A card scaled
   into 200px is legible as a picture but not as a phone number, so
   the way out has to be obvious rather than discoverable. */
.cardlink {
  position: relative;
  text-decoration: none;
  cursor: zoom-in;
}

.cardzoom {
  position: absolute;
  right: 0;
  bottom: 0;
  background: var(--ink);
  color: #fff;
  font-family: var(--font);
  font-size: 11px;
  font-weight: 700;
  letter-spacing: .08em;
  text-transform: uppercase;
  padding: 4px 8px;
}

/* ---- card scanner -------------------------------------------
   Full-screen, because you are holding the phone one-handed in an
   aisle and the corners need the whole width to be draggable.
   -------------------------------------------------------------- */
.cropwrap {
  position: fixed;
  inset: 0;
  z-index: 100;
  background: rgba(21, 23, 26, .94);
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 12px;
  padding: 12px;
}

.cropstage { position: relative; }
.cropview { display: block; max-width: 100%; }

.cropsvg {
  position: absolute;
  inset: 0;
  pointer-events: none;
}

.croppoly {
  fill: rgba(240, 106, 42, .18);
  stroke: var(--hi);
  stroke-width: 2;
}

.crophandle {
  position: absolute;
  width: 26px;
  height: 26px;
  border-radius: 50%;
  background: var(--hi);
  border: 2px solid var(--ink);
  /* The finger covers the corner it is dragging, so the handle is
     offset visually by the ring while the point stays at its centre. */
  box-shadow: 0 0 0 2px rgba(255, 255, 255, .55);
  touch-action: none;
  cursor: grab;
}

.crophint {
  color: #fff;
  font-size: 14px;
  margin: 0;
  text-align: center;
}

.crophint.bad { color: var(--hi); font-weight: 700; }

.cropbar { display: flex; gap: 10px; }

.cropreview {
  display: flex;
  align-items: center;
  justify-content: center;
  max-width: 100%;
  border: 2px solid var(--hi);
  background: #fff;
}

.cropreview img {
  display: block;
  max-width: min(88vw, 620px);
  max-height: 62vh;
  object-fit: contain;
}

/* ---- pull to refresh -----------------------------------------
   Only ever visible in the installed app, where there is no address
   bar to reload from.
   -------------------------------------------------------------- */
.ptr {
  position: fixed;
  top: -46px;
  left: 0;
  right: 0;
  height: 46px;
  display: flex;
  align-items: center;
  justify-content: center;
  background: var(--ink);
  z-index: 90;
  opacity: 0;
  pointer-events: none;
  transition: opacity .12s linear;
}

.ptr-label {
  font-family: var(--font);
  font-size: 12px;
  font-weight: 700;
  letter-spacing: .08em;
  text-transform: uppercase;
  color: #fff;
}

/* Full-screen photo. An overlay rather than a new tab: the installed
   app runs standalone and has no tabs to come back from, and a freshly
   cropped card is a data URL, which browsers will not open as a
   document. Used for product photos and business cards alike. */
.lightbox {
  position: fixed;
  inset: 0;
  z-index: 110;
  background: rgba(21, 23, 26, .96);
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 14px;
  padding: 14px;
  padding-bottom: max(14px, env(safe-area-inset-bottom));
  cursor: zoom-out;
}

.lightbox img {
  max-width: 100%;
  max-height: 80vh;
  object-fit: contain;
  border: 2px solid var(--hi);
  background: #fff;
}

.lightbox-bar {
  display: flex;
  align-items: center;
  gap: 12px;
}

/* White on the accent is 3.07:1 — under AA for small text, fine for
   this 14px bold label at button size. */
.lightbox-btn {
  font-family: var(--font);
  font-size: 14px;
  font-weight: 700;
  letter-spacing: .08em;
  text-transform: uppercase;
  color: #fff;
  background: var(--accent-grad);
  border: 0;
  border-radius: var(--pill);
  /* 44px so it is hittable one-handed with the phone in the other. */
  min-width: 44px;
  min-height: 44px;
  padding: 0 12px;
  cursor: pointer;
}

.lightbox-count {
  font-family: var(--font);
  font-size: 12px;
  font-weight: 700;
  letter-spacing: .08em;
  text-transform: uppercase;
  color: #fff;
  min-width: 48px;
  text-align: center;
}

/* ---- billing & plans ------------------------------------------- */

.plans { display: grid; grid-template-columns: repeat(3, 1fr); gap: 16px; margin-bottom: 20px; }
@media (max-width: 980px) { .plans { grid-template-columns: 1fr; } }

.plan { display: flex; flex-direction: column; }
.plan .nm { font: 700 17px var(--font-display); margin-bottom: 10px; }
.plan .price { font: 700 34px var(--font-display); letter-spacing: -.02em; }
.plan .price span { font: 500 14px var(--font); color: var(--faint); }
.plan .terms { font: 500 13px var(--font); color: var(--muted); margin: 6px 0 18px; }
.plan .btn { width: 100%; margin-bottom: 18px; }
.plan ul { list-style: none; margin: 0; padding: 0; display: grid; gap: 11px; }
.plan li {
  display: flex; align-items: flex-start; gap: 10px;
  font: 500 13.5px var(--font);
}
.plan li::before {
  content: "\2713";
  color: var(--ok-ink); font-weight: 700; flex: none;
}
/* The plan this business is on. */
.plan.current { border-color: var(--accent); }
.plan .onit {
  display: inline-block; margin-bottom: 18px;
  padding: 10px 14px; border-radius: var(--r-btn);
  background: var(--accent-soft-bg); color: var(--accent-ink);
  font: 700 13px var(--font); text-align: center; width: 100%;
}

.paymethod {
  display: flex; align-items: center; gap: 12px;
  padding: 14px 16px; border-radius: 12px;
  background: var(--surface-sunk);
  font: 600 13.5px var(--font);
}

/* ---- platform admin -------------------------------------------- */

.tenant .nm { font: 700 14px var(--font); }
.tenant .meta { font: 500 12px var(--font); color: var(--faint); }

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

.authtabs {
  display: flex; gap: 4px; margin-bottom: 20px;
  background: var(--surface-sunk); padding: 4px; border-radius: 12px;
}
.authtabs a {
  flex: 1; text-align: center; padding: 12px; border-radius: var(--r-sm);
  font: 600 13.5px var(--font); color: var(--muted); text-decoration: none;
}
.authtabs a.active { background: var(--ink); color: #fff; font-weight: 700; }

/* ---- settings: sub-nav and pane -------------------------------- */

.setgrid { display: grid; grid-template-columns: 240px 1fr; gap: 16px; align-items: start; }
@media (max-width: 899px) { .setgrid { grid-template-columns: 1fr; } }

.setnav { display: flex; flex-direction: column; gap: 2px; }
@media (max-width: 899px) {
  /* A row of chips on a phone: a 240px column of links above the pane
     would push the content off the first screen. */
  .setnav {
    flex-direction: row; overflow-x: auto; gap: 6px;
    margin-bottom: 14px; padding-bottom: 4px;
    scrollbar-width: none;
  }
  .setnav::-webkit-scrollbar { display: none; }
  .setnav a { white-space: nowrap; }
}
.setnav a {
  padding: 10px 14px; border-radius: var(--r-sm);
  font: 600 13.5px var(--font); color: var(--text); text-decoration: none;
}
.setnav a:hover { background: var(--surface); color: var(--text); }
.setnav a.active {
  background: var(--accent-soft-bg); color: var(--accent-ink); font-weight: 700;
}
.setpane { min-width: 0; }

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

   The handoff's inventory table. A CSS grid rather than a <table>
   because the same markup has to become one card per line on a
   phone, and a table cannot do that without duplicating every row.
   --------------------------------------------------------------- */

.stockgrid {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--r);
  overflow: hidden;
}
.stockgrid .head, .stockgrid .row {
  display: grid;
  grid-template-columns: 0.6fr 1.1fr 1.6fr 1.3fr 0.7fr 0.8fr 1fr;
  gap: 12px; align-items: center;
  padding: 14px 22px;
}
.stockgrid .head {
  background: var(--surface-sunk);
  font: 700 11px var(--font);
  letter-spacing: .03em; text-transform: uppercase;
  color: var(--faint);
}
.stockgrid .row {
  border-top: 1px solid var(--rule);
  font: 600 13px var(--font);
  color: inherit; text-decoration: none;
}
.stockgrid .row:hover { background: var(--surface-sunk); }
.stockgrid .pic {
  width: 38px; height: 38px; border-radius: 9px;
  background: var(--bg) center/cover no-repeat;
}
.stockgrid .metawrap { display: contents; }
/* The markup is in the phone's order, so desktop names its columns
   rather than relying on source order. */
.stockgrid .pic     { grid-column: 1; }
.stockgrid .code    { grid-column: 2; }
.stockgrid .item    { grid-column: 3; }
.stockgrid .shop    { grid-column: 4; }
.stockgrid .hand    { grid-column: 5; }
.stockgrid .reorder { grid-column: 6; }
.stockgrid .retail  { grid-column: 7; }
.stockgrid .code, .stockgrid .shop { color: var(--muted); }
.stockgrid .item { color: var(--accent-ink); font-weight: 700; }
.stockgrid .reorder { color: var(--faint); }
.stockgrid .retail { color: var(--accent-ink); font-weight: 700; }
.stockgrid .r { text-align: right; }
.stockgrid .u { color: var(--faint); font-weight: 500; }
.stockgrid .hand.low { color: var(--danger-ink); }
.stockgrid .hand.low .u { color: var(--danger-ink); }

/* Money in a stat card is long; it gets its own smaller size rather
   than a style attribute repeated at each call site. */
.stat .value.money { font-size: 22px; }

/* ---- the list card --------------------------------------------

   The handoff's shape for shops, order lists and anything else that
   is a list of records: one bordered card, rows divided by hairlines,
   the whole row clickable with a chevron at its end. Not a table —
   tables are for the numeric screens.
   --------------------------------------------------------------- */

.listcard {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--r);
  overflow: hidden;
}
.listcard > a, .listcard > .row-item {
  display: flex; align-items: center; gap: 16px;
  padding: 18px 22px;
  border-bottom: 1px solid var(--rule);
  text-decoration: none; color: inherit;
}
.listcard > a:last-child, .listcard > .row-item:last-child { border-bottom: 0; }
.listcard > a:hover { background: var(--surface-sunk); }

.listcard .avatarsq {
  width: 44px; height: 44px; flex: none;
  border-radius: 12px; background: var(--bg) center/cover no-repeat;
}
.listcard .grow { flex: 1; min-width: 0; }
/* Both are spans in the markup — without this they share a line and
   the stall number runs on from the shop name. */
.listcard .nm { display: block; font: 700 14.5px var(--font); }
.listcard .nm .zh { color: var(--faint); font-weight: 500; }
.listcard .meta {
  display: block;
  font: 500 12.5px var(--font); color: var(--muted);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.listcard .side { text-align: right; }
.listcard .side .a { font: 600 13px var(--font); }
.listcard .side .b { font: 500 12px var(--font); color: var(--faint); }
.listcard .num { width: 70px; text-align: right; font: 600 13px var(--font); }
.listcard .money { width: 100px; text-align: right; font: 700 13.5px var(--font);
                   font-variant-numeric: tabular-nums; }
/* The item count, as a soft accent pill. */
.listcard .count {
  width: 34px; height: 34px; flex: none;
  border-radius: var(--pill);
  background: var(--accent-soft-bg); color: var(--accent-ink);
  font: 700 12px var(--font-display);
  display: flex; align-items: center; justify-content: center;
}
.listcard .chev { color: rgba(21, 22, 28, .3); flex: none; display: flex; }

@media (max-width: 640px) {
  .listcard > a, .listcard > .row-item { padding: 14px 16px; gap: 12px; }
  .listcard .side, .listcard .num, .listcard .chev { display: none; }
}

/* A back link above a detail page's heading. */
/* The exports in a row, with the one state-changing action pushed
   to the far end so it is never tapped instead of a download. */
.actionbar {
  display: flex; gap: 10px; flex-wrap: wrap;
  align-items: center; margin-bottom: 16px;
}
.actionbar .soft {
  margin-left: auto;
  background: var(--accent-soft-bg); color: var(--accent-ink);
  border-color: transparent;
}
.actionbar .soft:hover { background: #ffdcc9; color: var(--accent-ink); }
@media (max-width: 640px) { .actionbar .soft { margin-left: 0; } }

/* The logo picker in Business profile. */
.logorow { display: flex; gap: 16px; align-items: flex-start; flex-wrap: wrap; }
.logorow .grow { flex: 1 1 260px; min-width: 0; }
.logoframe {
  width: 84px; height: 84px; flex: none;
  border-radius: 16px;
  border: 1px solid var(--border);
  background: var(--surface-sunk);
  display: flex; align-items: center; justify-content: center;
  overflow: hidden;
}
/* Same rule in the picker, so what you see there is what the rail
   will show. */
.logoframe img { width: 100%; height: 100%; object-fit: contain; display: block; padding: 6px; }
/* A horizontal lockup needs a landscape frame; a square one crops the
   wordmark to nothing. */
.logoframe.wide { width: 190px; height: 64px; }
.logoframe .icosvg { width: 34px; height: 34px; }

/* The legal pages: a readable measure, and text that breathes. A
   privacy policy set at the app's default 15px across a 1400px screen
   is technically present and practically unread. */
.prose { max-width: 720px; }
.prose .card { padding: 22px; }
.prose h2 { margin-bottom: 10px; }
.prose p { margin: 0 0 12px; line-height: 1.65; }
.prose p:last-child { margin-bottom: 0; }

/* The site footer. Quiet — it is legal furniture, not navigation, so
   it sits at the RIGHT end of the content rather than starting the
   line where the eye lands first. */
.sitefoot {
  display: flex; align-items: center; justify-content: flex-end;
  gap: 8px 20px; flex-wrap: wrap;
  padding: 20px 24px calc(28px + var(--safe-bottom));
  border-top: 1px solid var(--rule);
  font: 500 12.5px var(--font);
  color: var(--faint);
}
.sitefoot nav { display: flex; gap: 18px; flex-wrap: wrap; }
/* Not var(--accent-ink): a footer of orange links reads as a call to
   action, and these are the opposite of that. */
.sitefoot a { color: var(--muted); text-decoration: none; }
.sitefoot a:hover { color: var(--accent-ink); }
.sitefoot .copy { font-variant-numeric: tabular-nums; }
/* A separator only when both sit on one line. */
.sitefoot .copy::before { content: "·"; margin-right: 20px; color: var(--rule); }

@media (max-width: 899px) {
  /* Clear of the floating tab bar, or the copyright sits under it. */
  .sitefoot {
    padding: 18px 16px calc(110px + var(--safe-bottom));
    justify-content: flex-start; gap: 6px 14px;
  }
  .sitefoot nav { flex: 1 1 100%; }
  .sitefoot .copy::before { content: none; }
}

.backlink {
  display: inline-flex; align-items: center; gap: 6px;
  font: 700 13px var(--font); margin-bottom: 14px;
}

/* ================================================================
   THE PHONE — LAST IN THE FILE, DELIBERATELY.

   Every rule below overrides one defined earlier, and CSS resolves a
   tie by source order. When this block sat next to the tab bar in the
   middle of the file, base rules written after it silently won and
   half the phone layout reverted — the greeting kept the name, the
   hero kept its breakdown, the rows kept their yuan column. Anything
   phone-specific belongs here, at the bottom.
   ================================================================ */

@media (max-width: 899px) {
  /* ---- the phone, matched to the handoff's mobile frame -------- */

  .shell {
    width: 100%; margin: 0; border: 0; border-radius: 0;
    box-shadow: none; min-height: 100vh;
    background: var(--surface-sunk);
  }
  .rail { display: none; }

  /* 56px, business chip left, avatar right, one hairline under. */
  .topbar {
    height: 56px; padding: 0 18px;
    background: var(--surface-sunk);
    border-bottom: 1px solid var(--border);
    justify-content: space-between;
  }
  .topbar .brand { display: none; }   /* the handoff shows no wordmark here */
  .topsearch { display: none; }       /* nor a search field */
  .topdiv { display: none; }
  .topbar .spacer { display: none; }

  .topbar .orgchip {
    display: flex; margin: 0; padding: 0; gap: 8px;
    background: none; border: 0;
  }
  .topbar .orgchip .initials {
    width: 26px; height: 26px; border-radius: 8px; font-size: 11px;
  }
  .topbar .orgchip .name { color: var(--text); font: 700 13.5px var(--font); }
  .topbar .orgchip .role { display: none; }

  .topuser .nm, .topuser .out { display: none; }
  .topuser .avatar { width: 30px; height: 30px; font-size: 11px; }

  /* The handoff's phone header carries no sync state, because its app
     is never offline. This one's whole point is that it is — so the
     chip stays, but ONLY when it has something to say. Synced and it
     disappears, which is the handoff's header exactly. */
  .syncchip { padding: 5px 10px; font-size: 11px; }
  .syncchip[data-state="online"] { display: none; }

  main { padding: 18px 18px calc(130px + var(--safe-bottom)); }

  h1.page { font-size: 24px; margin-bottom: 4px; }
  .sub { font-size: 13px; margin-bottom: 16px; }
  .pagehead { display: block; margin-bottom: 0; }
  .pagehead .btn { display: none; }   /* the tab bar carries the action */
  .deskonly { display: none; }

  .card { padding: 16px; }

  /* .deskonly is one class; a rule like `.rows .money .cny` is two and
     wins on specificity. Anywhere that happens, say it again here. */
  .rows .money .deskonly { display: none; }
  .pricebox .btnrow .btn .deskonly { display: none; }
  .tabbar .capture > span:not(.ico) { display: none; }

  /* KPI cards scroll sideways rather than wrapping into a block of
     six — the handoff's row, and it keeps the hero above the fold. */
  .grid.stats {
    display: flex; gap: 10px; overflow-x: auto;
    padding-bottom: 2px; margin-bottom: 14px;
    scrollbar-width: none;
  }
  .grid.stats::-webkit-scrollbar { display: none; }
  .stat { flex: none; min-width: 110px; padding: 14px 16px; border-radius: 14px; }
  .stat .label { font-size: 10px; margin-bottom: 6px; }
  .stat .value { font-size: 20px; }
  .stat .note { display: none; }

  .pricebox { padding: 20px; margin-bottom: 14px; }
  .pricebox .head { display: block; }
  .pricebox .formula { font-size: 11.5px; }
  .pricebox .big { font-size: 30px; margin-bottom: 14px; }
  .pricebox .btnrow { display: flex; gap: 8px; }
  .pricebox .btnrow .btn { flex: 1; padding: 11px; min-height: 0; font-size: 12.5px; }
  /* The handoff's phone hero is the figure and two actions, nothing
     else. The breakdown stays on desktop. */
  .pricebox .split { display: none; }

  .orgchip { margin-bottom: 0; }

  .tabbar {
    position: fixed; z-index: 55;
    left: 12px; right: 12px; bottom: calc(12px + var(--safe-bottom));
    display: flex; align-items: center; justify-content: space-around;
    height: 62px; padding: 0 6px;
    border-radius: var(--pill);
    background: rgba(255, 255, 255, .82);
    -webkit-backdrop-filter: blur(18px);
    backdrop-filter: blur(18px);
    border: 1px solid var(--border);
    box-shadow: var(--shadow-lift);
  }
  .tabbar a, .tabbar button {
    flex: 1; border: 0; background: none; cursor: pointer;
    display: flex; flex-direction: column; align-items: center; gap: 3px;
    padding: 0; min-height: 44px; justify-content: center;
    font: 600 10px var(--font);
    color: var(--faint); text-decoration: none;
  }
  .tabbar a.active, .tabbar button.active { color: var(--accent); }
  .tabbar .icosvg { width: 21px; height: 21px; }   /* the handoff's size */

  /* Raised, filled, and lifted clear of the bar. */
  .tabbar .capture {
    flex: none;
    width: 54px; height: 54px; margin-top: -22px;
    border-radius: 50%;
    background: var(--accent-grad);
    color: #fff;
    box-shadow: 0 8px 20px rgba(225, 74, 31, .42);
    gap: 0;
  }
  .tabbar .capture .icosvg { width: 24px; height: 24px; }
  /* The LABEL is hidden, not every span — .ico is a span too, and
     hiding it left a solid orange disc with nothing in it. */
  .tabbar .capture > span:not(.ico) { display: none; }
}

@media (max-width: 899px) {
  /* One card per line, as the handoff's phone frame has it. The head
     row has nothing to label once the columns are gone. */
  .stockgrid {
    background: none; border: 0; border-radius: 0; overflow: visible;
  }
  .stockgrid .head { display: none; }
  .stockgrid .row {
    display: flex; align-items: flex-start; gap: 12px;
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: 14px;
    padding: 14px;
    margin-bottom: 10px;
  }
  .stockgrid .pic { width: 44px; height: 44px; border-radius: 10px; flex: none; }
  /* A real element here, not `contents`: its children have to flow as
     a line rather than be placed in cells. */
  .stockgrid .metawrap { display: block; flex: 1; min-width: 0; }
  .stockgrid .item {
    display: inline; font-size: 13.5px;
  }
  .stockgrid .retail { font-size: 13px; text-align: right; flex: none; }
  /* Code, shop and stock read as one line under the name. */
  .stockgrid .code, .stockgrid .shop, .stockgrid .hand {
    font: 500 12px var(--font); color: var(--muted);
  }
  .stockgrid .item { display: block; margin-bottom: 3px; }
  /* Columns are a desktop concern; on a phone these are words in a
     sentence and must not be placed. */
  .stockgrid .pic, .stockgrid .code, .stockgrid .item, .stockgrid .shop,
  .stockgrid .hand, .stockgrid .reorder, .stockgrid .retail { grid-column: auto; }
  .stockgrid .code::after, .stockgrid .shop::after { content: " · "; }
  .stockgrid .hand::after { content: " on hand"; }
  .stockgrid .reorder { display: none; }
}
