/* public/css/00-tokens.css */
/* Shoppilot — the stylesheet, assembled by GET /app.css from the partials of
 * public/css/ in filename order (NN-component.css, so name order is cascade
 * order). There is no build step, no web font and no dark mode (DESIGN.md
 * "Elevation": the UI is flat -- depth comes from borders and background
 * contrast, not shadows; a card on a phone takes the one hairline
 * --shadow-card, and only things that float take the level-2 one, like a
 * confirm dialog). The one glass is the frosted bar: --bar-tint with a
 * --bar-highlight edge (DESIGN.md "Frosted bars").
 *
 * This partial holds the custom properties in :root: the tokens of
 * docs/DESIGN.md, copied verbatim. It is the only partial that declares :root.
 * Every colour, radius and font elsewhere is used through its token, never as a
 * raw hex, so a change lands in one place.
 *
 * Narrow screens are the point, not an afterthought: a store manager reads
 * /actions on a 375px phone. Each partial carries the narrow-screen overrides of
 * its own selectors in a media query after its base rules: under 720px the page
 * padding tightens, controls grow to a 44px touch target, and .table becomes a
 * stack of cards -- severity and store, then what/why, then the action, then
 * owner and due date, then the form -- one field per line with its column
 * heading above it.
 */

:root{
  --ink:#1A1A1A;--accent:#B5381B;--bg:#F4F7FA;--surface:#FFFFFF;--surface-muted:#F0F3F6;--line:#DFE6EE;
  --brand-orange:#F47118;--brand-red:#BB2528;
  --brand-gradient:linear-gradient(90deg,var(--brand-orange),#E24A21,var(--brand-red));
  --text:#1F2D3D;--text-secondary:#44546A;--text-muted:#5B6B7B;--text-subtle:#617080;
  --ok:#2E7D32;--ok-tint:#E2F0D9;--ok-on-tint:#1B5E20;
  --warn:#A16207;--warn-tint:#FFF8E1;--warn-border:#FFE08A;
  --bad:#C62828;--bad-tint:#FDECEA;--bad-border:#F5C2C0;
  --sev-high:#C2410C;
  --chart-baseline:#C9D6E3;--chart-actual:#2F75B5;--chart-marker:#E8590C;
  --demo:#FFC000;--demo-text:#3A2A00;
  --radius-sm:6px;--radius-md:10px;--radius-pill:999px;
  /* The font stack (DESIGN.md "Typography"): the Apple system font first for
     Latin and digits, then the Traditional Chinese faces for the Han characters
     it has no glyph for -- PingFang TC on Apple devices, Noto Sans TC on phones
     without it -- then the platform's own UI font. font-optical-sizing is left at
     its initial value, auto, so a face with an optical-size axis picks its cut
     from the font size; no partial sets it to none. */
  --font:-apple-system,BlinkMacSystemFont,"SF Pro Text","PingFang TC","Noto Sans TC",system-ui,sans-serif;

  /* Frosted bars (DESIGN.md): --ink at 72% for the bar, and white at 12% for the
     1px highlight along its top edge. Colours are tokens, so no other partial
     writes an rgba() of its own. */
  --bar-tint:rgba(26,26,26,.72);--bar-highlight:rgba(255,255,255,.12);
  /* Elevation level 1 (DESIGN.md): the hairline shadow a card takes on a phone. */
  --shadow-card:0 1px 2px rgba(0,0,0,.04),0 1px 1px rgba(0,0,0,.06);
  /* Elevation level 2 (DESIGN.md): things that float, and nothing else. */
  --shadow-float:0 4px 16px rgba(23,50,77,.12);
  /* The one easing curve (DESIGN.md "Pressed state"): fast off the mark and slow
     into rest, so a press answers the finger at once. Used for transform only. */
  --ease-out:cubic-bezier(.23,1,.32,1);
  /* Spacing steps, so the base-4 scale is not re-typed as magic numbers. */
  --space-2:4px;--space-3:6px;--space-4:8px;--space-5:10px;--space-6:12px;
  --space-7:14px;--space-8:16px;--space-9:22px;--space-10:28px;
}
/* public/css/10-base.css */
/* Base: the reset, the document defaults, headings and links, the focus ring and
 * the skip link. Rules that apply to every page whatever it shows. */

*,*::before,*::after{box-sizing:border-box}

html{-webkit-text-size-adjust:100%}

body{
  margin:0;
  background:var(--bg);
  color:var(--text);
  font:400 14px/1.5 var(--font);
}

/* Small type is tracked open, as optical type is (tracking loosens as size
   shrinks): text of 13px or smaller takes letter-spacing +0.01em, larger text
   none. A length in em is fixed at the element that declares it and
   inherited as pixels, so this is declared on every element and works out from
   that element's own size. The calc is positive (1000px per px under 13px, plus
   1px so exactly 13px still counts) at 13px and below and negative above, and
   clamp() holds it to 0 .. .01em. No caption has to name itself here, so a new
   one is tracked without a rule. */
*,*::before,*::after{
  letter-spacing:clamp(0px,calc((13px - 1em) * 1000 + 1px),.01em);
}

/* Every number on every screen lines up in its column. */
.kpi__value,.store-card__figures,.table td,.table th,.chart,.pill,.badge{
  font-variant-numeric:tabular-nums;
}

/* Page titles are 28px / 700 on a phone: the largest type on the screen, two
   steps above the 14px body and eight above the 20px section heading, so the
   page is named at a glance, and 700 holds a title that size together. Only the
   size, the weight, the tracking and the wrap are set: a title sits on the bar's
   own colour, which it takes from the bar, so no colour is named here. The desk
   size is put back in the 768px block below; the weight is 700 there too and
   needs no reset.
   Display type is tracked tight, as optical type is, and a title is the largest
   type on the screen, so it takes letter-spacing -0.022em: -0.616px at 28px, a
   step tighter than the section heading's -0.014em. The h1 selector outranks the
   universal tracking rule above, which would leave 28px type at none, so the
   title names its own tracking. The desk tracking is put back in the 768px block
   below.
   A title longer than the line it is given, as on a 320px screen, is broken on
   balanced lines: text-wrap:balance has the browser even the lines out, so a title
   of two lines is two lines of near-equal length and not one full line and a stub of
   a word, which reads as an accident. It is the shorthand, which also puts the wrap
   mode to wrap, and it is CSS alone, with no break written into the title. Where
   a title is not allowed to wrap, balancing has no lines to even out and changes
   nothing; the top bar's own rules (public/css/20-topbar.css) say whether its
   title wraps or is cut with an ellipsis. The desk wrap is put back in the 768px
   block below. */
h1{font-size:28px;font-weight:700;letter-spacing:-.022em;text-wrap:balance}

/* Section headings are 20px / 600 on a phone: the size steps up from the 14px
   body so a heading is found at a glance, and 600 keeps it from shouting at
   that size. Display type is tracked tight, as optical type is, so the heading
   takes letter-spacing -0.014em: -0.28px at 20px. The h2 selector outranks the
   universal tracking rule above, which would leave a 20px heading at none. The
   desk size, weight and tracking are put back in the 768px block below. */
h2{
  margin:var(--space-9) 0 var(--space-5);
  color:var(--ink);
  font-size:20px;font-weight:600;line-height:1.4;
  letter-spacing:-.014em;
}

a{color:var(--accent)}

/* One visible focus ring for everything focusable (DESIGN.md "Buttons"). */
a:focus-visible,button:focus-visible,input:focus-visible,select:focus-visible,
textarea:focus-visible,[tabindex]:focus-visible{
  outline:2px solid var(--accent);
  outline-offset:2px;
}

/* Keyboard escape hatch, hidden until it is focused. */
.skip-link{
  position:absolute;left:-9999px;top:0;z-index:10;
  padding:var(--space-6) var(--space-7);
  background:var(--surface);color:var(--ink);
  border-radius:0 0 var(--radius-sm) 0;
}
.skip-link:focus{left:0}

/* 768px and wider: the desk layout is set as it was, so the tracking above is a
   phone rule and is put back to the browser's own, the page title goes back to
   its desk 20px with no tracking and its lines are no longer balanced (wrap is
   the initial value of text-wrap), and the section heading goes back to its desk
   16px / 700 with no tracking. The title and the heading each need their own
   tracking reset: the h1 and h2 rules outrank the universal one, so that one
   cannot reach them. */
@media (min-width:768px){
  *,*::before,*::after{letter-spacing:normal}
  h1{font-size:20px;letter-spacing:normal;text-wrap:wrap}
  h2{font-size:16px;font-weight:700;letter-spacing:normal}
}

/* v1 ships no animation (DESIGN.md "Elevation"); if a transition is ever
   added, this keeps it from moving for the people who asked it not to. */
@media (prefers-reduced-motion:reduce){
  *,*::before,*::after{
    transition-duration:.01ms !important;
    animation-duration:.01ms !important;
    animation-iteration-count:1 !important;
    scroll-behavior:auto !important;
  }
}
/* public/css/15-links.css */
/* Links: the link a page writes with no class of its own and that is the whole of
 * its cell, line or list item -- the 編輯 link of a table row, the way back from an
 * error page, a destination on /more, the week links of the roster editor. It is a
 * control in its own right, not a word in a sentence, so it is a touch target.
 * Mobile first: the base rule is the phone's, and the rule for 768px and wider sits
 * in a min-width media query after it, putting back what the phone rule changed.
 * Nothing here moves, so there is no motion to take away under
 * prefers-reduced-motion. */

/* Under 768px such a link is a 44px by 44px target at least (the Mobile tokens table
   in docs/DESIGN.md). An <a> is inline by default, and an inline box ignores
   min-height and min-width, so the link is an inline-flex box: it takes both, keeps
   sitting in its line like the inline box it replaces (two links in one paragraph,
   as the week links are, stay side by side), and centres its label in the target.
   The selector is the parent and its link, `td>a`, so it is the link that is the
   cell's own content and not any link in the page: the classed links are the
   components' (.btn, .segmented__link, the nav tabs), whose own rules are more
   specific and win over this one. The ring is the page's (10-base.css), not set
   here. */
td>a,p>a,li>a{
  display:inline-flex;align-items:center;
  min-width:44px;min-height:44px;
}

/* ---------------------------------------- 768px and wider: the inline link */

/* A pointer needs no 44px target, so the link is the inline one it was. The display
   goes back to the browser's own with revert, as the buttons' does (80-buttons.css),
   and not to a value written here; the minimums go back to auto, their initial
   value. */
@media (min-width:768px){
  td>a,p>a,li>a{display:revert;min-width:auto;min-height:auto}
}
/* public/css/20-topbar.css */
/* Top bar: the dark bar every shell page opens with (src/core/layout.mjs
 * .topbar) -- logo, titles, the DEMO badge and the language switch.
 * Mobile first: the base rules are the phone's, and the rules for 768px and wider
 * sit in a min-width media query after them, each putting back what the phone
 * rules changed. */

/* Under 768px the bar is one 52px row, pinned to the top of the viewport: it is
   sticky, not fixed, so it still takes its place at the head of the page and the
   content starts right below it, and it stays in view while the page scrolls
   under it. Its height is set, not left to its content, so a long title, a
   missing logo or a wrapped language switch can never make it taller: the title
   shrinks to one line and is cut with an ellipsis instead. The 3px rule
   (.topbar::after) is drawn over the bottom 3px, so the title fits in the 49px
   above it. The business-date meta line is not in the bar: it hangs from the
   bar's bottom edge as a caption (.topbar__meta below), and the 26px of margin
   under the bar is the room the caption takes, so the page starts below it. The
   bar stays below .skip-link (z-index 10 in 10-base.css), so the link still
   shows over the bar when it takes focus. The gaps between the logo, the titles
   and the aside are 8px, not the desktop 12px and 14px, so the row still fits a
   360px phone.
   The phone bar is frosted glass, not a solid: --bar-tint, which is --ink at 72%
   (rgba(26,26,26,.72), written in 00-tokens.css and nowhere else), over the page
   scrolling under it, blurred 20px with its colours pushed to 180% so the bar
   reads as lit rather than just dull. backdrop-filter is declared twice, with the
   -webkit- prefix first and without it after, because Safari still wants the
   prefixed name and a browser that knows both takes the standard one. The page is
   light, so the bar renders no darker than --ink at 72% over white (#5A5A5A) and
   white on it is still 6.90:1, white at 85% 5.53:1 (DESIGN.md "Frosted bars").
   The bar's top edge is lit: a 1px --bar-highlight, white at 12% (written in
   00-tokens.css and nowhere else), drawn as an inset shadow along the top, where
   the light catches the glass, so it takes no height and moves nothing in the
   row, and the bar has no border of its own. It is the same line as along the
   top of the tab bar (30-nav.css). It is painted over the bar's background and
   under what the bar holds, so no text or control is touched by it, and a line
   carries no information, so it has no contrast to meet.
   The filter is written once, here, in the one rule that paints --bar-tint, which
   is where the flat-stylesheet check in tests/core/layout.test.mjs allows it. The
   desk bar below and the phone bar under prefers-reduced-transparency (the query
   after the desk rules) are solid --ink again and do not switch the filter off:
   an opaque background paints over everything the blur has made of the page
   behind it, so the filter has nothing left to show. The lit edge is not part of
   the glass's blur, so the solid phone bar keeps it, as the tab bar does, and
   only the desk bar, which is not glass at all, loses it.
   Nothing here animates, so there is no motion to take away under
   prefers-reduced-motion. */
.topbar{
  position:sticky;top:0;z-index:5;
  box-sizing:border-box;height:52px;
  margin-bottom:26px;
  display:flex;align-items:center;justify-content:space-between;
  gap:var(--space-4);
  padding:0 var(--space-8);
  background:var(--bar-tint);
  -webkit-backdrop-filter:blur(20px) saturate(180%);
  backdrop-filter:blur(20px) saturate(180%);
  box-shadow:inset 0 1px 0 var(--bar-highlight);
  color:#fff; /* on --ink this is 17.4:1, and 6.90:1 on the frosted bar over white */
}
/* The 3px rule along the bottom edge: --brand-gradient, orange at the left to
   red at the right. It is drawn over the bar's bottom edge, so the bar keeps
   the height its rules give it, and it carries no information, so it takes no
   clicks and no text. The bar is its containing block, whether sticky or
   relative. It is the only place the gradient is used. */
.topbar::after{
  content:"";
  position:absolute;left:0;right:0;bottom:0;
  height:3px;
  background:var(--brand-gradient);
  pointer-events:none;
}
/* The logo sits at the left and links home. Its width follows the artwork. The
   titles take the free space, which keeps the aside at the right. The two
   controls in the bar, the logo link and the language links, are 44px by 44px, the
   touch target of the Mobile tokens table in docs/DESIGN.md: 44px tall, which fits
   in the 49px above the rule, and 44px wide at least. The mark is 21.7px wide (149 x
   192 at 28px), so the link is wider than the mark: the mark sits at the link's
   left edge, where the page's 16px inset puts it, and the title starts 22px
   further right than the mark alone would put it. The mark has no width and height
   attributes, so the link's own 44px is also the room kept for it, and the title
   does not jump right when the file arrives. The link has no underline, because
   when the mark does not load its alt text is the link's text. */
.topbar__logo{
  display:flex;align-items:center;
  flex-shrink:0;
  min-width:44px;min-height:44px;
  margin-right:var(--space-2);
  color:inherit;text-decoration:none;
  border-radius:var(--radius-sm);
}
.topbar__logo img{display:block;width:auto}
/* The link holds two images and shows one: under 768px the mark, because the
   horizontal wordmark is too wide for the bar; from 768px up the 32px wordmark
   (the media query below). The wordmark is display:none here, which also takes
   it out of the accessibility tree: it is decoration, and the mark carries the
   link's name. Both selectors name the class as well as the element, so the
   `.topbar__logo img` rule above cannot override them whatever its order. */
.topbar__logo .topbar__wordmark{display:none;height:32px}
/* The mark is 28px high; its width follows the artwork. When /brand/mark.png
   fails to load, the browser puts its alt text where it was, as the text of the
   link: the bar's white, because the link takes its colour from the bar and
   `color:inherit` here keeps the image on it, 13px bold on one line. The line is
   shorter than the link's 44px, so the link stays 44px and the bar, whose 52px
   is set on .topbar and not left to its content, stays 52px. Nothing here
   applies while the mark shows: the font and nowrap rules only style alt text.
   The height is the only size: a width or an aspect ratio too would make the
   browser lay the missing image out as a box of that shape and cut the text. */
.topbar__logo .topbar__mark{
  height:28px;
  color:inherit;
  font-size:13px;font-weight:700;line-height:1.2;
  white-space:nowrap;
}
/* The page's ring colour (--accent) is 2.9:1 on the dark bar, so every link and button
   in it (the logo, the language switch) takes the bar's own white instead. */
.topbar a:focus-visible,.topbar button:focus-visible{outline:2px solid #fff}
/* A flex item may shrink below its text, or the ellipsis below never shows. */
.topbar__titles{min-width:0;margin-right:auto}
.topbar__title,.topbar__meta{overflow:hidden;white-space:nowrap;text-overflow:ellipsis}
.topbar__title{
  margin:0;
  font-size:20px;font-weight:700;line-height:1.2;
}
/* The business-date meta line under 768px: a 13px caption below the bar, not a
   second line inside it. It is taken out of the bar's flow and hung from the
   bar's bottom edge (top:100%, the bar being its containing block, whether
   sticky or relative) over the page's own --bg, so the bar's 52px holds the
   title alone. It is 26px tall -- 4px, one 18px line, 4px -- which is the
   margin .topbar keeps free for it, and it is one line cut with an ellipsis
   (the rule above), so a long date can never be taller than that room. Its
   16px side padding is the bar's, so the caption starts where the logo does.
   It is not on the dark bar, so its text is --text-muted (5.1:1 on --bg), not
   the bar's white; 13px, regular weight, set in the bar's own stacking layer so
   page content scrolls under it as it does under the bar. */
.topbar__meta{
  position:absolute;top:100%;left:0;right:0;
  box-sizing:border-box;height:26px;
  margin:0;padding:var(--space-2) var(--space-8);
  background:var(--bg);
  color:var(--text-muted);
  font-size:13px;font-weight:400;line-height:18px;
}
.topbar__aside{
  display:flex;align-items:center;gap:var(--space-4);
  flex-shrink:0;
}

/* The only yellow in the product: 示範數據 DEMO, while data is synthetic. */
.demo-badge{
  display:inline-block;
  padding:2px var(--space-4);
  background:var(--demo);
  color:var(--demo-text);
  border-radius:var(--radius-pill);
  font-size:11px;font-weight:700;line-height:1.6;
  white-space:nowrap;
}

.lang-switch{
  display:flex;align-items:center;gap:var(--space-3);
  font-size:13px;
}
/* Each language link is a 44px by 44px target, its label centred in it: 中文 is 26px
   wide and would otherwise be a target a thumb has to aim at. An inline-flex box
   takes the minimums an inline link would ignore. */
.lang-switch a{
  display:inline-flex;align-items:center;justify-content:center;
  min-width:44px;min-height:44px;
  color:rgba(255,255,255,.85);text-decoration:none;
}
.lang-switch a[aria-current="true"]{color:#fff;font-weight:700;text-decoration:underline}
.lang-switch span{color:rgba(255,255,255,.5)}

/* ----------------------------------------------------- 768px and wider */

/* The desk bar: relative, so it scrolls away with the page and has no page
   passing under it to frost, so it is solid --ink, as it was before the phone's
   glass. The background goes back, and the lit edge goes with the tint
   (box-shadow:none): a desk bar is flat, so a line of white along its top would be
   a change to how it looks. The filter the phone rule declared (both spellings)
   is left as it is, because it blurs a backdrop this opaque --ink hides, so the
   bar renders the same pixels as the plain --ink bar and no rule outside the one
   that paints --bar-tint has to mention the filter.
   It is as tall as its content with 18px above and below, the logo and the language links as tall as
   their text (a pointer needs no 44px target), the titles on as many lines as
   they take, and the wider gaps. The wordmark replaces the mark, which is
   hidden by clipping it to a point instead of display:none, so it is still the
   link's name to a screen reader. The meta line goes back into the bar, under
   the title: in the flow again, on the bar's own --ink, in the bar's 85% white
   and with no room kept for a caption below the bar. */
@media (min-width:768px){
  .topbar{
    position:relative;top:auto;z-index:auto;
    height:auto;
    margin-bottom:0;
    align-items:flex-start;
    gap:var(--space-6);
    padding:18px var(--space-10);
    background:var(--ink);
    box-shadow:none;
  }
  .topbar__logo{display:block;align-self:center;min-width:0;min-height:0}
  .topbar__logo .topbar__wordmark{display:block}
  .topbar__logo .topbar__mark{
    position:absolute;
    width:1px;height:1px;
    clip-path:inset(50%);
  }
  .topbar__titles{min-width:auto}
  .topbar__title,.topbar__meta{overflow:visible;white-space:normal;text-overflow:clip}
  .topbar__title{line-height:1.3}
  .topbar__meta{
    position:static;
    height:auto;
    margin-top:var(--space-2);padding:0;
    background:none;
    color:rgba(255,255,255,.85);
    line-height:1.4;
  }
  .topbar__aside{gap:var(--space-7)}
  .lang-switch a{display:inline;min-width:0;min-height:0}
}

/* Less transparency, when the user asks for it: the phone bar drops its tint and
   is solid --ink, as the desk bar is, so what scrolls under it can no longer
   show through, blurred or not: the solid background hides the filter's result
   in both spellings, Safari's prefixed one included. White on --ink is 17.40:1,
   white at 85% 12.81:1. The query comes after the desk rules and sets the value
   they do, so the two agree from 768px up. */
@media (prefers-reduced-transparency:reduce){
  .topbar{background:var(--ink)}
}

/* More contrast, when the user asks for it. The top bar is dark, so its
   secondary text goes to full white rather than --text (1.2:1 on --ink);
   --text is for the light surfaces only. The phone's meta line is the one text
   of this partial that is not on the bar but on the page's --bg, so there it
   goes to --text (13:1) and white would vanish; the query after it puts the
   white back for the meta line once it is inside the bar again, from 768px up.
   The phone bar also gets a real 1px border, white at 40%, the same line the tab
   bar has (30-nav.css), so the glass has an edge a reader can see. It is the bar's
   top edge, the lit one, and not its bottom: the bottom edge is where the 3px rule
   and the meta caption are laid, both measured from the bar's padding box, so a
   border there would sit under them. The 12% lit edge is an inset shadow, which
   is drawn inside the border, so it stays as a second, fainter line just under
   it. The border is inside the bar's 52px
   (box-sizing is border-box), and the controls in it are 44px, which still fit
   in the 48px between it and the rule. The colour is --surface mixed 40% into
   transparent, which is rgba(255,255,255,.4); it is derived from the palette's
   white because no partial but 00-tokens.css writes a colour. The query after it
   takes the border away again from 768px up, where the desk bar is as it was. */
@media (prefers-contrast:more){
  .topbar{border-top:1px solid color-mix(in srgb,var(--surface) 40%,transparent)}
  .topbar__meta{color:var(--text)}
  .lang-switch a{color:#fff}
}
@media (min-width:768px) and (prefers-contrast:more){
  .topbar{border-top:0}
  .topbar__meta{color:#fff}
}
/* public/css/30-nav.css */
/* Nav tabs: the page tabs, and the signed-in user with their logout link
 * (src/core/layout.mjs .nav).
 * Mobile first: the base rules are the phone's, where the nav is a tab bar fixed
 * to the bottom of the viewport, in the same frosted --ink as the top bar
 * (20-topbar.css). The rules for 768px and wider sit in a min-width media query
 * at the end, each putting back what the phone rules changed: there the nav is
 * the white row under the top bar, the tabs at the left and the user line at the
 * right. */

/* Under 768px the nav is a tab bar fixed to the viewport's bottom edge, across
   its whole width: it stays in view while the page scrolls under it, and it is
   out of the page's flow, so the content starts right below the top bar. Its
   bottom padding is the phone's safe area -- the home indicator of a notched
   iPhone, which the page reaches under because the viewport meta says
   viewport-fit=cover -- and it is 0px where the browser has no safe area or no
   env(), so the bar is flush with the viewport bottom there. The tabs sit above
   that padding, and the bar's own frosted background fills it, so the bar runs
   unbroken to the viewport's edge under the home indicator. z-index 5 is the
   top bar's layer: the two never overlap, both stay below .skip-link (z-index
   10 in 10-base.css), and page content, which sets none, scrolls under both.
   The bar is the top bar's frosted --ink (DESIGN.md "Frosted bars"): --bar-tint,
   which is --ink at 72%, over the page scrolling under it, blurred 20px with its
   colours pushed to 180%, and the 1px --bar-highlight along its top edge, where
   the light catches it, drawn as an inset shadow so it takes no height: it is
   the edge the bar has against the page, so the bar has no border of its own.
   backdrop-filter is declared twice, with the -webkit- prefix first and without
   it after, because Safari still wants the prefixed name and a browser that
   knows both takes the standard one. It is written here, in the one rule that
   paints --bar-tint, which is where the flat-stylesheet check in
   tests/core/layout.test.mjs allows it. The row from 768px up is --surface again
   and does not switch the filter off, as the desk top bar does not: an opaque
   background paints over everything the blur has made of the page behind it, so
   the filter has nothing left to show there. A frosted bar is lightest over a
   white page, where it renders #5A5A5A, so the tabs' text is white (6.90:1) or
   white at 70% (4.38:1 there, 4.55:1 over --surface-muted and 8.97:1 on the solid
   bar of prefers-reduced-transparency), never --text-secondary, which is dark on
   a dark bar. The 70% is a little under AA over a white page: it is the share
   the phone's tabs are specified at, and the bar's tint, which would have to
   rise from 72% to 74% to lift it, is 00-tokens.css's and the top bar's too.
   The one thing here that animates is a tab's press (.nav__tabs a below), a
   transform and nothing else, which the query under the reader's preferences
   takes away under prefers-reduced-motion. */
.nav{
  position:fixed;left:0;right:0;bottom:0;z-index:5;
  display:block;
  padding-bottom:env(safe-area-inset-bottom, 0px);
  background:var(--bar-tint);
  -webkit-backdrop-filter:blur(20px) saturate(180%);
  backdrop-filter:blur(20px) saturate(180%);
  box-shadow:inset 0 1px 0 var(--bar-highlight);
}
.nav__tabs{
  display:flex;
  margin:0;padding:0;
  list-style:none;
}
/* The tabs share the bar's width equally, whatever their labels: a flex basis of
   0 and a minimum width of 0, so a longer label wraps inside its tab instead of
   taking room from the others. Each item is a flex row itself, which makes its
   link as tall as the tallest tab, so every tab is one tap target the height of
   the bar, with the current tab's rule at the same height as the rest. */
.nav__tabs li{
  display:flex;flex:1 1 0;min-width:0;
}
/* A tab is a 49px target at least, the height of an iOS tab bar and over the
   44px minimum, and 44px wide at least: the width is what the equal shares
   above give it (64px of a 320px phone, with four tabs and the 更多 tab), and the
   minimum is the floor under it, so no label and no number of tabs can leave a
   target narrower than a thumb. It is the link that carries both, not its list
   item, because the link is what is tapped. The icon and label are stacked in a
   column and centred in it: the 24px icon (.nav__icon below) over the label,
   which makes a tab about 52px tall. The 11px label wraps to a second line when a translation is too long for
   the room (five tabs share a 320px phone: four and the 更多 tab), and a single
   word too long for the tab is broken rather than left to run under its
   neighbour. The 3px rule along the bottom is transparent, so the current tab's
   rule changes no height.
   The text is on the frosted --ink bar, so it is white: --surface is the one
   white of the palette, and 20-topbar.css's white on its own bar. A tab at rest
   is white at 70%, written as the link's opacity of .7 (the rule at rest is
   transparent, so only the label and its icon fade) and not as an rgba(),
   because no partial but 00-tokens.css writes a colour. The tab under the
   pointer and the current tab are full white, and the current one is also 600
   with a white 3px rule along its bottom: --accent is 1.16:1 on the bar, too
   faint for the one mark of where you are, so the rule takes the text's colour,
   and the bright icon and label, the heavier label and the rule say it without
   colour. The weight is the phone's: the desktop row's current tab stays 700,
   and the rule of 768px and wider says so. The tabs take a white ring when
   they are focused, for the same reason: the page ring (--accent, 10-base.css)
   is about as faint on the bar. It is drawn 4px inside the tab's edge, because
   the tabs meet each other, the viewport's sides and, where there is no safe
   area, its bottom edge, which would cut a ring drawn outside them. Only the
   ring's colour and offset change: its 2px and its style are the page's.
   The press (DESIGN.md "Pressed state"): while a tab is :active -- from the moment
   the finger goes down -- the whole link, icon, label and rule together, scales to
   .97 and eases there and back over 120ms with --ease-out. Only transform is
   transitioned, so the tab's box, and the bar's, stay where they are, and the
   hover's opacity stays instant. The transition is on the resting rule so the
   release eases as well as the press. The link is a flex box on the phone and a
   block from 768px up, both of which take a transform, so the press is the same
   at every width and the rule at the end of this file has nothing to put back. */
.nav__tabs a{
  display:flex;flex:1;flex-direction:column;align-items:center;justify-content:center;
  min-width:44px;min-height:49px;
  padding:var(--space-3) var(--space-2);
  border-bottom:3px solid transparent;
  color:var(--surface);opacity:.7;
  font-size:11px;line-height:1.2;
  overflow-wrap:anywhere;
  text-align:center;text-decoration:none;
  transition:transform 120ms var(--ease-out);
}
.nav__tabs a:hover{opacity:1}
.nav__tabs a:active{transform:scale(.97)}
.nav__tabs a[aria-current="page"]{
  border-bottom-color:var(--surface);
  opacity:1;font-weight:600;
}
.nav__tabs a:focus-visible{
  outline-color:var(--surface);
  outline-offset:-4px;
}
/* The icon above a tab's label: an inline SVG the shell writes inside the link
   (tabIcon() in src/core/layout.mjs), 24px by its own width and height
   attributes and drawn in currentColor, so it is always the tab's text colour --
   white at 70%, full white on hover and on the current tab, on the frosted bar
   -- and this partial sets no colour for it. It is a block so no line box leaves a gap under it, and
   it does not shrink: in the column above a flex item would give up height
   before the label did. The desktop row is text, so the rule at the end of this
   file does not display it. */
.nav__icon{display:block;flex:none}
/* The signed-in user and role line, with the logout link in it. Under 768px it
   is not displayed at all -- GET /more carries the user line, the language
   switch and the logout link on a phone -- and the rule at the end of this file
   displays it again from 768px up. */
.nav__user{
  display:none;
  margin:0;
  color:var(--text-muted);
  font-size:13px;
  text-align:right;
}
/* The logout link is a control in its own right, so it keeps the 44px target in
   both directions wherever the line is displayed under 768px, as the tabs do:
   an inline-flex box, because an inline link ignores min-height. The line is
   hidden on a phone today, so this changes no pixel there; the rule at the end of
   this file puts the inline link of the desk line back. */
.nav__logout{
  display:inline-flex;align-items:center;
  min-width:44px;min-height:44px;
  color:var(--accent);
}

/* ------------------------------------------------ under 768px: phone only */

/* Rules for a phone alone, which have nothing to put back at 768px, so
   they are not part of the mobile-first base above. */
@media (max-width:767px){
  /* The shell tags the language switch of a signed-in page with data-on-phone=
     "more" (ON_MORE_ATTRIBUTE in src/core/layout.mjs), and a phone does not
     display it in the top bar: /more has it, with the user line and the logout
     link. This is the one rule that says so, here and not in 20-topbar.css,
     which owns .lang-switch and keeps its own display untouched; the query ends
     where the rule at the end of this file begins, so the switch is the top
     bar's again at 768px. A page with no user does not carry the tag, so the
     sign-in page keeps its language switch on a phone: there is no /more for it
     to move to. */
  [data-on-phone="more"]{display:none}

  /* The same tag on a tab: the shell writes the first six tabs the role may use,
     and tags each one after the fourth because the bar holds four tabs and the
     更多 tab, and /more lists the rest (the row has the fifth and sixth too). It
     is its own rule, naming the list item, because
     the base rule `.nav__tabs li` sets display:flex on every item and, a class
     and an element against the attribute's one, would win over the rule above. */
  .nav__tabs li[data-on-phone="more"]{display:none}

  /* A signed-in page whose route shows this role no tab at all: the shell marks
     its nav (nav--no-tabs), and with the user line not displayed on a phone
     nothing is left to put in the bar, so it is not drawn rather than left as
     an empty strip along the bottom edge. */
  .nav--no-tabs{display:none}
}

/* ---------------------------------------------- the reader's preferences */

/* Less motion, when the user asks for it: a tab no longer transitions its
   transform, so the press is not eased in or out. The scale itself stays -- a
   pressed tab still answers at once, as a change of state and not a movement --
   and only the 120ms between the two sizes goes. The tab's own rule, and not left
   to the page's blanket 0.01ms (10-base.css): there is no transition here at all,
   not a very short one. It names the same selector as the rule it overrides and
   comes after it, and the 768px rules set no transition, so it holds at every
   width. */
@media (prefers-reduced-motion:reduce){
  .nav__tabs a{transition:none}
}

/* Less transparency, when the user asks for it: the phone bar drops its tint and
   is solid --ink, as the top bar does (20-topbar.css), so what scrolls under it
   can no longer show through, blurred or not: the opaque background hides the
   filter's result in both spellings, Safari's prefixed one included. White on
   --ink is 17.40:1, white at 70% 8.97:1. The query comes before the 768px rules,
   not after them as the top bar's does, because the row is --surface and not
   --ink: the rule of 768px and wider sets its own background, which wins over
   this one at the same weight, so the row is not made dark. */
@media (prefers-reduced-transparency:reduce){
  .nav{background:var(--ink)}
}

/* More contrast, when the user asks for it: the tabs at rest go from white at 70%
   to full white, as the top bar's secondary text does, and the bar gets a real
   1px border along its top, where the lit edge is only 12% white. The border is
   white at 40%, so it is the same line on the frosted bar and on the solid one
   the query above makes: it is --surface mixed 40% into transparent, which is
   rgba(255,255,255,.4), and the colour is derived from the palette's white
   because no partial but 00-tokens.css writes a colour. The same border is on
   the top bar (20-topbar.css). It takes a line of the bar's height, which the
   72px the page keeps free under it (40-page.css) has to spare. The row from
   768px up puts the border back to 0 and the tabs' opacity to 1 itself, so this
   query reaches nobody there either. */
@media (prefers-contrast:more){
  .nav{border-top:1px solid color-mix(in srgb,var(--surface) 40%,transparent)}
  .nav__tabs a{opacity:1}
}

/* ------------------------------------ 768px and wider: the desktop nav row */

/* From 768px up the nav is the row under the top bar: in the page's flow again,
   the tabs at the left and the signed-in user and role line, with the logout
   link in it, at the right. It is white again, --surface, and not the frosted
   tint: the page no longer scrolls under it, so there is nothing to frost, and
   its lit edge goes with the tint. A tab is back to its text-sized link: 14px
   text, 12px 14px of padding and a 4px gap, left-aligned, as tall as its text,
   --text-secondary at full strength, and --ink under the pointer and on the
   current tab, whose rule is --accent again and whose weight is 700 again, where
   the phone's is 600. The ring is the page's again, 2px outside the link. The
   bar's rule is its bottom edge, a 1px --line, and the safe-area padding goes
   with the rest of the padding. A phone has the user line on /more instead
   (src/core/routes/more.mjs): the base rule hides the line, and this one puts it
   back. The row has the first six tabs the role may use and then the 更多 tab,
   which this block leaves as the base rules have it: it is a tab like the others
   and every width displays it, because /more holds what the row does not -- the
   tabs after the sixth, and the account controls, 更改密碼 among them. The shell
   writes no more tabs than that, and each can shrink and wrap (the row's items
   are `flex:0 1 auto`, and nothing here sets a width, a nowrap or an overflow),
   so the row stays inside the page and never scrolls sideways. */
@media (min-width:768px){
  .nav{
    position:static;
    display:flex;align-items:center;justify-content:space-between;
    gap:var(--space-6);
    padding:0 var(--space-10);
    background:var(--surface);
    box-shadow:none;
    border-top:0;
    border-bottom:1px solid var(--line);
  }
  .nav__tabs{align-items:center;gap:var(--space-2)}
  .nav__tabs li{display:block;flex:0 1 auto;min-width:auto}
  .nav__tabs a{
    display:block;
    min-width:auto;min-height:0;
    padding:var(--space-6) var(--space-7);
    color:var(--text-secondary);opacity:1;
    font-size:14px;line-height:1.5;
    overflow-wrap:normal;
    text-align:start;
  }
  .nav__tabs a:hover{color:var(--ink)}
  .nav__tabs a[aria-current="page"]{
    border-bottom-color:var(--accent);
    color:var(--ink);
    font-weight:700;
  }
  .nav__tabs a:focus-visible{
    outline-color:var(--accent);
    outline-offset:2px;
  }
  /* The icons are the phone bar's: the row's tabs are text only, as they were. */
  .nav__icon{display:none}
  .nav__user{display:block}
  .nav__logout{display:inline;min-width:auto;min-height:0}
}
/* public/css/40-page.css */
/* Page shell: the centred column every page's content sits in
 * (src/core/layout.mjs .page). */

/* Mobile first: the base rule is the phone's. Under 768px the nav is a tab bar
   fixed to the viewport's bottom edge and out of the page's flow
   (30-nav.css), so the page runs underneath it. The page therefore ends 72px
   plus the safe area above its own bottom edge: the bar is 50px tall (a 49px tab
   and the 1px rule along its top) plus the home-indicator inset it pads itself
   by, and the rest is the gap between the last row and the bar. Scrolled to the
   end, the last row sits clear of the bar on every phone, notched or not.
   The safe-area call carries a 0px fallback, so where the browser has no safe
   area the padding is a plain 72px; the plain 72px line before it is what a
   browser that cannot parse env() at all keeps, as it drops the calc(). Both
   come after the padding shorthand, which would otherwise overwrite them. */
.page{
  max-width:1400px;
  margin:0 auto;
  padding:var(--space-8);
  padding-bottom:72px;
  padding-bottom:calc(72px + env(safe-area-inset-bottom, 0px));
}

/* ------------------------------------- 768px and wider: the desktop column */

/* The nav is the row under the top bar again, in the flow, so there is no bar
   for the page to clear: the shorthand sets all four sides and, coming after the
   base rule, takes the bottom padding back from 72px to 22px. */
@media (min-width:768px){
  .page{padding:var(--space-9) var(--space-10)}
}
/* public/css/50-cards.css */
/* Cards: the surfaces and the grids that lay them out, then the KPI blocks and
 * store cards the dashboard is made of. */

/* --------------------------------------------------------------- surfaces */

/* A card, a KPI card and a store card have 16px corners, 16px of inner padding and
   the hairline --shadow-card on a phone, where a card is most of the screen's
   width and the softer corner, the roomier inset and the faint lift off the page
   suit it. The shadow is the token, never a value written here, and it is set
   and not animated: a card does not move. The 768px block at the end of the
   partial puts --radius-md, the 14px padding of a card and a KPI card (a store
   card is 16px at every width) and the flat no-shadow card back, so the desk
   layout does not change. */
.card{
  padding:var(--space-8);
  background:var(--surface);
  border:1px solid var(--line);
  border-radius:16px;
  box-shadow:var(--shadow-card);
}
.card__empty{
  margin:0;
  color:var(--text-muted);
  font-size:13px;
}

.grid{
  display:grid;gap:var(--space-6);
  margin:0;
  padding:0;
  list-style:none;
}
/* On a phone the KPI cards sit two to a row and the store cards one to a row.
   minmax(0,1fr) rather than 1fr keeps the two KPI columns equal whatever a
   value is: a bare 1fr stretches a track to its content's width, which would
   push the grid past the screen's edge. */
.grid--kpi{grid-template-columns:repeat(2,minmax(0,1fr))}
.grid--stores{grid-template-columns:1fr}

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

.kpi{
  padding:var(--space-8);
  background:var(--surface);
  border:1px solid var(--line);
  border-radius:16px;
  box-shadow:var(--shadow-card);
}
.kpi--attention{border-left:4px solid var(--warn)}
.kpi--critical{border-left:4px solid var(--bad)}
.kpi__label{margin:0;color:var(--text-muted);font-size:12px}
/* On a phone a KPI card is half a screen wide, and a figure such as $1,234,567
   has no place to break, so at a fixed 26px it would run out of the card. The
   value scales with the screen instead (6vw, never under 20px or over 28px) and
   stays on one line. The 768px block puts the desk size back. */
.kpi__value{margin:var(--space-2) 0 0;font-size:clamp(20px,6vw,28px);font-weight:600;line-height:1.2}
.kpi__sub{margin:var(--space-2) 0 0;color:var(--text-muted);font-size:12px}

.store-card{
  padding:var(--space-8);
  background:var(--surface);
  border:1px solid var(--line);
  border-radius:16px;
  box-shadow:var(--shadow-card);
}
.store-card__head{display:flex;align-items:baseline;justify-content:space-between;gap:var(--space-6)}
.store-card__title{margin:0;font-size:18px;font-weight:700}
.store-card__figures{
  display:flex;flex-wrap:wrap;gap:var(--space-7);
  margin:var(--space-7) 0 0;
}
.store-card__figure{margin:0;font-size:18px;font-weight:600}
.store-card__change{margin:var(--space-2) 0 0;color:var(--text-muted);font-size:12px}

/* ------------------------------------- 768px and wider: the desk layout */

/* From 768px up the grids are what they were before the phone layout: as many
   columns as fit, never a fixed number. A KPI card is 180px at least, so the grid
   is four across once it is 756px wide (four cards and three 12px gaps) and five
   across at 948px, where the day's five cards share one row. How wide the grid is
   comes from the page column's padding, not from this partial: it sets the card's
   minimum size and nothing else. */

@media (min-width:768px){
  .grid--kpi{grid-template-columns:repeat(auto-fit,minmax(180px,1fr))}
  .grid--stores{grid-template-columns:repeat(auto-fit,minmax(300px,1fr))}
  .kpi__value{font-size:26px}
  .card{padding:var(--space-7);border-radius:var(--radius-md);box-shadow:none}
  .kpi{padding:var(--space-7);border-radius:var(--radius-md);box-shadow:none}
  .store-card{border-radius:var(--radius-md);box-shadow:none}
}
/* public/css/60-tables.css */
/* Tables: .table, its row tints and the what/why/meta cells. Mobile first: on a
 * phone a table is a list of compact rows, one line for each cell, and the rules
 * for 768px and wider sit in a min-width media query at the end, where a table is
 * a table again. */

.table-wrap{width:100%;overflow-x:auto}

.table{
  width:100%;
  border:1px solid var(--line);
  border-collapse:collapse;
  background:var(--surface);
  font-size:13px;line-height:1.45;
}
.table th,.table td{
  padding:9px var(--space-5);
  text-align:left;
  vertical-align:top;
  border-bottom:1px solid var(--line);
}
.table thead th{
  background:var(--ink);
  color:#fff; /* 17.4:1 on --ink */
  font-size:13px;font-weight:700;
  border-bottom:0;
}
.table tbody tr:last-child th,.table tbody tr:last-child td{border-bottom:0}
.table .row--bad{background:var(--bad-tint)}
.table .row--warn{background:var(--warn-tint)}

/* The first line of an alert is "what happened"; the muted line under it is
   "why". Both wrap -- the why text is never truncated (DESIGN.md "Don't"). */
.cell-what{font-weight:600}
.cell-why{
  display:block;
  margin-top:var(--space-2);
  color:var(--text-muted);
  font-size:12px;font-weight:400;
  white-space:normal;
}
.cell-meta{color:var(--text-muted);font-size:12px}

/* ------------------------------------------------ the phone: 375px first */

/* The rules above are the table's own look. Below them are the phone's: a table
   that may stack (every one but .table--scroll) becomes a list. Each row of the
   table is one compact box, and each cell in it is one line: the column heading
   at the left in --text-muted, the value at the right. The heading is the cell's
   data-label, drawn by ::before. It floats rather than sharing a flex row with
   the value because a value is not one element -- a name, a <br> and a muted
   line; a pill; a figure -- and a flex row would spread those pieces apart. A
   value too long for the line wraps, still at the right, and is never cut. A
   value that cannot wrap to fit beside the heading, such as an address, an
   invoice number or a pill, moves down below it, to a line of its own as wide as
   the row (CSS 2.1 9.5: a line box too narrow for its content is shifted below the
   float). Only a value still wider than the row is broken, inside the row, so
   nothing pushes out of the row's box or past the screen's edge. */
.table:not(.table--scroll){
  display:block;
  border-width:0;
  background:none;
}
.table:not(.table--scroll) thead{
  position:absolute;
  width:1px;height:1px;
  overflow:hidden;
  clip-path:inset(50%);
  white-space:nowrap;
}
.table:not(.table--scroll) tbody,
.table:not(.table--scroll) tr,
.table:not(.table--scroll) th,
.table:not(.table--scroll) td{display:block}
.table:not(.table--scroll) tr{
  margin-bottom:var(--space-6);
  padding:var(--space-2) var(--space-7);
  background:var(--surface);
  border:1px solid var(--line);
  border-radius:var(--radius-md);
}
.table:not(.table--scroll) th,
.table:not(.table--scroll) td{
  padding:var(--space-4) 0;
  border-bottom:0;
  /* A word broken only when it is wider than the row. It is this and not
     word-break:break-all, which would break a word beside the heading and so
     never let it move under it first. It is on every cell, not just the labelled
     ones: a cell with no heading, or a row header, can hold the same address. */
  overflow-wrap:anywhere;
}
/* A hairline between the lines of a row. It is a top border so that the
   last-row rule above, which clears a bottom border, cannot take it away. */
.table:not(.table--scroll) tr>*+*{border-top:1px solid var(--line)}
/* flow-root keeps the floated heading inside its line when the value is empty. */
.table:not(.table--scroll) td[data-label]{
  display:flow-root;
  text-align:right;
}
.table:not(.table--scroll) td[data-label]::before{
  content:attr(data-label);
  float:left;
  max-width:50%;
  margin-right:var(--space-6);
  color:var(--text-muted);
  font-size:12px;font-weight:400;
  /* 12px at 1.55 is the height of a 13px line at 1.45, so the heading and the
     first line of the value sit on one baseline. */
  line-height:1.55;
  text-align:left;
}
/* A row that was tinted for a reason keeps an edge marker as a list row. */
.table:not(.table--scroll) .row--bad{border-left:4px solid var(--bad)}
.table:not(.table--scroll) .row--warn{border-left:4px solid var(--warn)}

/* A table with no rows says so in one sentence, and every empty state in the app
   carries the .card__empty class (50-cards.css gives it the muted 13px). Standing
   in the card in place of the table, that sentence is already one full-width line.
   Set in the table as a cell across its columns, it would be a row like any other
   -- boxed, and with a heading in front if the cell has a data-label -- so here the
   row gives up its box and the cell its heading: the sentence is the one line it
   is in the card, at the left, wrapping if it must and never cut. Two rules and
   not one list, because a browser that cannot read :has() drops the whole rule it
   is in, and the cell's rule must outlive that. Neither sets a display, which the
   768px block would then have to give back. */
.table:not(.table--scroll) tr:has(>.card__empty){
  margin-bottom:0;
  padding:0;
  background:none;
  border:0;
  border-radius:0;
}
.table:not(.table--scroll) td.card__empty{
  padding:0;
  text-align:left;
}
.table:not(.table--scroll) td.card__empty::before{content:none}

/* A wide table that cannot stack scrolls sideways instead. */
.table--scroll{min-width:560px}

/* More contrast, when the user asks for it: the edge of a row and the lines in it
   become unambiguous. It sits before the 768px block, which gives a desk table
   its own lines back. */
@media (prefers-contrast:more){
  .table:not(.table--scroll) tr{border-color:var(--text)}
  .table:not(.table--scroll) tr>*+*{border-top-color:var(--text)}
}

/* ------------------------------------- 768px and wider: the desk table */

/* From 768px up a table is a table again, as it was before the phone's rules: the
   header row shown, a row of cells, the tint on the whole row, no heading in
   front of a value. Each rule here undoes the rule of the same selector above.
   The border and the background of the table itself are not re-declared whole
   (only the width of the border is) so that 85-banners.css, which sets a table's
   border colour under prefers-contrast, still has the last word on it. */
@media (min-width:768px){
  .table:not(.table--scroll){
    display:table;
    border-width:1px;
    background:var(--surface);
  }
  .table:not(.table--scroll) thead{
    position:static;
    width:auto;height:auto;
    overflow:visible;
    clip-path:none;
    white-space:normal;
  }
  .table:not(.table--scroll) tbody{display:table-row-group}
  .table:not(.table--scroll) tr{
    display:table-row;
    margin-bottom:0;
    padding:0;
    background:none;
    border:0;
    border-color:inherit; /* what the browser gives a row, so the edge's colour is not left behind */
    border-radius:0;
  }
  .table:not(.table--scroll) th,
  .table:not(.table--scroll) td{
    display:table-cell;
    padding:9px var(--space-5);
    /* anywhere lets a column shrink to a single character, and a desk table sizes
       its columns from the least width each cell can have, so it goes back. */
    overflow-wrap:normal;
  }
  /* The header's cells never had a bottom border; the last row's, below in
     the base, still wins this one back to 0. */
  .table:not(.table--scroll) tbody th,
  .table:not(.table--scroll) tbody td{border-bottom:1px solid var(--line)}
  .table:not(.table--scroll) tr>*+*{border-top:0}
  .table:not(.table--scroll) td[data-label]{
    display:table-cell;
    text-align:left;
  }
  .table:not(.table--scroll) td[data-label]::before{content:none}
  /* An empty table's sentence is a cell of the table again, with a cell's padding.
     The row around it needs nothing: the phone's rule for it set what a desk row
     has, and its text was left-aligned and unheaded in both. */
  .table:not(.table--scroll) td.card__empty{padding:9px var(--space-5)}
  /* The tint is the row's marker again, so the edge goes and the tints, which the
     card's own background above had covered, are back. */
  .table:not(.table--scroll) .row--bad,
  .table:not(.table--scroll) .row--warn{border-left:0;border-left-color:inherit}
  .table:not(.table--scroll) .row--bad{background:var(--bad-tint)}
  .table:not(.table--scroll) .row--warn{background:var(--warn-tint)}

  .table--scroll{min-width:auto}
}
/* public/css/70-pills.css */
/* Pills and status text: severity and flag pills, and the open/done status words. */

.pill{
  display:inline-block;
  padding:2px var(--space-4);
  border-radius:var(--radius-pill);
  color:#fff;
  font-size:11px;font-weight:700;line-height:1.6;
  white-space:nowrap;
}
.pill--critical{background:var(--bad)}
.pill--high{background:var(--sev-high)}
.pill--medium{background:var(--warn)}
.pill--low{background:var(--text-subtle)}
.pill--ok{background:var(--ok)}
.pill--flag-unrostered,.pill--flag-no-clock-in{background:var(--bad)}
.pill--flag-late,.pill--flag-over-time{background:var(--warn)}
.pill--flag-low-yield{background:var(--text-subtle)}
.pill--flag-on-time{background:var(--ok-tint);color:var(--ok-on-tint)}

/* Status text: open is bad, done and dismissed are ok. */
.status{font-size:12px;font-weight:700}
.status--open{color:var(--bad)}
.status--done,.status--dismissed{color:var(--ok)}
.status--acknowledged,.status--in_progress{color:var(--text)}

/* More contrast, when the user asks for it. */
@media (prefers-contrast:more){
  .status--acknowledged{color:var(--text)}
}
/* public/css/75-segmented.css */
/* Segmented: the status filter of the action tracker
 * (src/plugins/actions/routes/tracker.mjs) -- a track of links, 全部 and one per
 * status, with the segment for the filter in force marked aria-current="page".
 *
 * Phone first: the six segments sit in a 3 x 2 grid, each a 44px target and
 * none of them off screen or behind a scroll, whatever the language. From 768px
 * up they are one row of links as wide as their words. Nothing here moves and
 * nothing is translucent, so there is no motion and no glass to give up for
 * prefers-reduced-motion or prefers-reduced-transparency. */

.segmented{
  display:grid;
  grid-template-columns:repeat(3,1fr);
  gap:var(--space-2);
  padding:var(--space-2);
  border:1px solid var(--line);
  border-radius:var(--radius-md);
  background:var(--surface-muted);
}
.segmented__link{
  display:flex;align-items:center;justify-content:center;
  min-width:44px;min-height:44px;
  padding:var(--space-3) var(--space-4);
  border-radius:var(--radius-sm);
  color:var(--text-secondary);
  font-size:14px;line-height:1.5;
  text-align:center;
  text-decoration:none;
}
.segmented__link:hover{color:var(--ink)}

/* Pressed: felt the moment the finger lands, by opacity alone. */
.segmented__link:active{opacity:.85}

/* The segment in force is more than a colour: a raised surface, ink text at 700
   and the accent rule the nav tabs use for the current tab. */
.segmented__link[aria-current="page"]{
  background:var(--surface);
  box-shadow:inset 0 -3px 0 var(--accent);
  color:var(--ink);
  font-weight:700;
}

/* Focus is the page's ring (10-base.css: 2px, offset 2px) and this partial has
   no :focus-visible rule of its own. The track's 4px padding and 4px gap are
   what the ring needs, so it is drawn whole between neighbours and inside the
   track's edge, and the track clips nothing. */

/* ------------------------------------ 768px and wider: one row of links */

@media (min-width:768px){
  .segmented{display:inline-flex}
  .segmented__link{min-height:32px;padding:var(--space-3) var(--space-7)}
}

/* More contrast, when the user asks for it: a real edge on the track, ink text
   on every segment, and a heavier edge instead of the accent rule on the
   current one. */
@media (prefers-contrast:more){
  .segmented{border-color:var(--ink)}
  .segmented__link{color:var(--ink)}
  .segmented__link[aria-current="page"]{box-shadow:inset 0 0 0 2px var(--ink)}
}
/* public/css/80-buttons.css */
/* Buttons: .btn and its primary, secondary, danger and block variants.
 * Mobile first: the base rules are the phone's, and the rules for 768px and wider
 * sit in a min-width media query after them, each putting back what the phone
 * rules changed. The one thing that moves is the press: a button scales to .97
 * while it is pressed, and the rule at the end of this file swaps that for an
 * opacity of .85, with no scale and no transition, under prefers-reduced-motion. */

/* Under 768px a button is a touch target of at least 44px in both directions (the
   Mobile tokens table in docs/DESIGN.md), whether it is a <button> or a link
   written as one (<a class="btn">: Cancel, Export, Log out on /more). The width is
   what a short label such as 取消 would not reach on its own. The link needs the
   display as well as the minimums: an <a> is inline by default, and an inline box
   ignores min-height, so the same class would leave a <button> 44px tall and a
   link 30px. An inline-flex box takes both, keeps sitting in a line of text like
   the inline box it replaces, and centres the label in the target, which a
   <button> already does by itself. The 5px of vertical padding stays: the 44px is
   a minimum, so a label that wraps keeps its room. The ring is the page's
   (10-base.css), not set here.
   The press (DESIGN.md "Pressed state"): while a button is :active -- from the
   moment the pointer or finger goes down, not when it lifts -- it scales to .97,
   and it eases there and back over 120ms with --ease-out. Only transform is
   transitioned, so no layout moves and the hover's filter stays instant. The
   transition is on the resting rule so the release eases as well as the press.
   A transform needs a box: the phone's inline-flex link has one, and so does a
   <button> everywhere, but the desk link is the inline box revert gives it
   (below), which takes no transform, so a link written as a button shows no press
   from 768px up. Making it inline-block to get one would move the desk page. */
.btn{
  display:inline-flex;align-items:center;justify-content:center;
  min-width:44px;min-height:44px;
  padding:5px var(--space-6);
  border:1px solid transparent;
  border-radius:var(--radius-sm);
  background:var(--surface);
  color:var(--text);
  font:400 13px/1.4 var(--font);
  cursor:pointer;
  transition:transform 120ms var(--ease-out);
}
.btn:hover{filter:brightness(.92)}
.btn:active{transform:scale(.97)}
.btn--primary{background:var(--accent);color:#fff}
.btn--secondary{border-color:var(--line)}
.btn--danger{background:var(--bad);color:#fff}
.btn--block{width:100%}

/* ------------------------------------- 768px and wider: the desk buttons */

/* A pointer needs no 44px target, so the button is the 32px one it was before the
   phone rules. Its display goes back to the browser's own with revert -- inline
   for a link, inline-block for a <button> -- rather than a value written here,
   because the two differ and each has to be what it was; the centring of the
   flex box is inert on either. */
@media (min-width:768px){
  .btn{display:revert;min-width:auto;min-height:32px}
}

/* More contrast, when the user asks for it: the secondary button is the one
   without a fill, so its only edge is the 1px --line, about 1.3:1 on white; it
   takes --text, which is 13:1, as the banners' and the cards' edges do
   (85-banners.css). The primary and danger buttons are filled and need nothing. */
@media (prefers-contrast:more){
  .btn--secondary{border-color:var(--text)}
}

/* Less motion, when the user asks for it: nothing about a button moves. The press
   is no longer a scale -- a button that shrinks under the finger is a movement --
   but a change of state: while it is :active the button is at .85 opacity, from the
   moment the pointer or finger goes down, and it is whole again on release. The
   scale is put back to none, the same declaration that shows a button at rest, so
   the rule above is overridden and not undone by a second transform. Nothing is
   eased: the button has no transition at all, not a very short one, which is why
   this is the partial's own rule and not left to the page's blanket 0.01ms
   (10-base.css).
   The hover's filter is put back to none while pressed. Hover darkens the button
   8% and a press fades it, and the two together -- a pointer that is over the
   button and down on it -- would leave white text on a button that is both darker
   and fainter: 4.28:1 on the danger button over white, 4.30:1 on the primary, under
   the 4.5:1 the label has to keep. With the filter gone only the fill fades -- the
   white label over a white page stays white -- and the press alone is 4.53:1 on the
   primary over white and 4.53:1 on the danger button over the page's --bg. The one
   pair under the line is the danger button over a white card, 4.49:1 as painted:
   the .85 is the design's, and the fill is --bad, whose value stays, so it is
   0.01 short and no rule here can close it.
   The rule names the same selector as the one it overrides and comes after it, so
   it wins at the same weight, and it sets no width: it holds at every width. */
@media (prefers-reduced-motion:reduce){
  .btn{transition:none}
  .btn:active{transform:none;opacity:.85;filter:none}
}
/* public/css/82-forms.css */
/* Forms: fields, inputs, selects and textareas, and the error line under a field.
 * Mobile first: the base rules of the controls are the phone's, and the rules for
 * 768px and wider sit in a min-width media query after them, each putting back
 * what the phone rules changed. */

.field{display:flex;flex-direction:column;gap:var(--space-3)}
.field label{color:var(--text-muted);font-size:12px}

/* Under 768px a control is set in 16px type and is at least 44px tall. The 16px
   is the floor, not a taste: iOS Safari zooms the page in when a field whose text
   is smaller gains focus, and leaves it zoomed until the user pinches back out,
   so a 12px field would push the form half off a 375px screen the moment it was
   tapped. The 44px is the touch target of the Mobile tokens table in
   docs/DESIGN.md; it is a minimum, so a taller control (a textarea given rows)
   keeps its height. The size is the font shorthand's, so the weight, the 1.4
   line height and the --font stack are written beside it, not inherited from
   whatever the control sits in: a form control does not inherit font by
   default. One rule serves all three controls and a textarea needs no rule of its
   own, since the phone gives it the same 44px as the others. */
input,select,textarea{
  width:100%;
  min-height:44px;
  padding:var(--space-2) var(--space-2);
  border:1px solid var(--line);
  border-radius:var(--radius-sm);
  background:var(--surface);
  color:var(--text);
  font:400 16px/1.4 var(--font);
}
input:disabled,select:disabled,textarea:disabled{background:var(--surface-muted)}

/* The server message goes here, under the field it belongs to. */
.field__error{margin:0;color:var(--bad);font-size:13px}
.form{display:flex;flex-direction:column;gap:var(--space-3);min-width:190px}

/* ------------------------------------- 768px and wider: the desk controls */

/* A pointer needs no 44px target, and a desk page has the room for 12px type, so
   the controls are the 28px, 12px fields they were before the phone rules; a
   textarea is the taller 60px it was, written after the rule it overrides. iOS
   does not zoom a desk screen, which is why the 16px is a phone rule. */
@media (min-width:768px){
  input,select,textarea{min-height:28px;font-size:12px}
  textarea{min-height:60px}
}

/* Buttons are 80-buttons.css's, and their phone rule is that partial's to make.
   Until it does, this keeps the 44px target the buttons had when this partial
   held it, so a phone is not left with 32px buttons in the meantime. */
@media (max-width:767px){
  .btn{min-height:44px}
}
/* public/css/85-banners.css */
/* Banners: the data-quality notice at the top of a page, a <details> element.
 * Mobile first: the base rules are the phone's, and the rules for 768px and wider
 * sit in a min-width media query after them. The closed banner is one line, its
 * <summary> ("N 間門店資料有問題"), and a 44px touch target; the opened one adds
 * the heading of a blocked day and a line for each issue. */

.banner{
  margin:0 0 var(--space-8);
  border:1px solid var(--line);
  border-radius:var(--radius-md);
  font-size:13px;
}
.banner--warn{background:var(--warn-tint);border-color:var(--warn-border)}
.banner--blocking{background:var(--bad-tint);border-color:var(--bad-border)}

/* The summary is the control: the whole width of the banner is the target, so a
   thumb needs no aim. The browser's own marker gives way to a chevron that turns
   with the banner's state, the only thing here that moves (transform alone). No
   outline is set here: the summary keeps the browser's own :focus-visible ring,
   which the corners below let follow the banner's rounded edge. */
.banner__summary{
  display:flex;align-items:center;gap:var(--space-4);
  min-height:44px;
  padding:var(--space-4) var(--space-8);
  border-radius:var(--radius-md);
  font-weight:700;
  list-style:none;
  cursor:pointer;
}
.banner__summary::-webkit-details-marker{display:none}
.banner__summary::before{
  content:"";
  flex:none;
  width:7px;height:7px;
  border-right:2px solid currentColor;border-bottom:2px solid currentColor;
  transform:rotate(-45deg);
  transition:transform .2s ease-out;
}
.banner[open] > .banner__summary::before{transform:rotate(45deg)}

.banner__body{padding:0 var(--space-8) var(--space-5)}
.banner__title{margin:0 0 var(--space-2);font-weight:700}
.banner__line{margin:0}
.banner__line + .banner__line{margin-top:var(--space-2)}

/* ----------------------------------------------------- 768px and wider */

@media (min-width:768px){
  .banner{margin-bottom:var(--space-9)}
  /* A pointer needs no 44px target: the closed banner is the 10px 16px strip
     DESIGN.md describes. */
  .banner__summary{min-height:0;padding:var(--space-5) var(--space-8)}
}

/* More contrast, when the user asks for it: edges become unambiguous. One rule
   for every bordered surface (cards, KPIs, store cards, tables and banners), so
   it sits after the last of them, and after the banner variants above, so it
   wins over their softer border. */
@media (prefers-contrast:more){
  .card,.kpi,.store-card,.table,.banner{border-color:var(--text)}
}
/* public/css/88-charts.css */
/* Charts: inline HTML/SVG bars -- baseline (4-week same-weekday average) and actual. */

.chart{display:flex;align-items:flex-end;gap:var(--space-2);height:120px;margin:var(--space-7) 0 0}
.chart__hour{display:flex;align-items:flex-end;gap:2px;flex:1;height:100%}
.chart__bar{flex:1;background:var(--chart-baseline);border-radius:2px 2px 0 0}
.chart__bar--actual{background:var(--chart-actual)}
.chart__dot{width:6px;height:6px;border-radius:var(--radius-pill);background:var(--chart-marker)}
.chart__labels{display:flex;gap:var(--space-2);color:var(--text-subtle);font-size:10px}
.chart__legend{margin:var(--space-6) 0 0;color:var(--text-muted);font-size:12px}
/* public/css/90-login.css */
/* Login: the sign-in card of /login (src/plugins/account/routes/login.mjs).
 * The card is a .card, so its surface, border, radius and padding are
 * 50-cards.css's; what is here is where it sits. */

/* Centred in the page column and no wider than 400px, on every screen size.
   max-width, not width: on a phone narrower than 400px the card takes the
   column's width instead of pushing the page into a horizontal scroll. It is
   the same rule at every width, so there is no media query to keep in step.

   The card's side margins on a phone are not set here. It sits straight in the
   page column (<main class="page">), whose padding is 16px a side under 768px
   (40-page.css), and `margin:0 auto` resolves to 0 whenever the card is as wide
   as the column, so on a 320px screen the card is 288px wide with 16px each
   side and nothing sticks out past it. A margin of its own would stack on the
   column's 16px and squeeze the card to 256px, so there is none. */
.login{
  max-width:400px;
  margin:0 auto;
}

/* Vertically the card is not placed at all, and that is the rule. It is the
   first thing in the page column, so on a phone (under 768px) it starts at the
   column's top padding, 16px under the top bar, and the empty screen is below
   it, not around it. Its vertical margin is the 0 of `margin:0 auto`, and it has
   no min-height, no offset and no viewport-height unit, so nothing can centre it
   in the screen.

   That is on purpose. When the iOS keyboard opens it covers the lower half of
   the screen and shrinks what is visible; a card centred in the full height
   would sit with the username field near the middle of the screen, behind the
   keyboard or scrolled up under the bar, where a card at the top keeps that
   field about 350px from the top of a 390px-wide phone. 100vh and its kin are
   the screen with the keyboard closed, so they are never a size the card is
   built from. Nothing in this partial is a rule for the page column or the body
   either: those are not the card's to centre it with.

   tests/core/css/login.test.mjs holds all of this. */

/* The logo is centred above the form, with 24px of space under it, on every
   screen size. It is the first thing in the card and the form follows it, so the
   24px is the space between the logo and the form's first row: the username label
   on the page as it is first drawn, and the refusal's message when the page is
   drawn again after a refused sign-in (the form's own 6px gap then spaces the
   message from the label).

   display:block, because an image is inline by default: it would sit on a line
   box, which text-align would centre but which leaves a few pixels of descender
   room under it, so the space below would not be 24px. As a block, the automatic
   side margins centre it, at the 200px its width attribute gives it, in the card's
   content box. The bottom margin is the whole of the gap: the form has no margin
   above it, and the card's padding is the space above the logo, not under it.

   24px is written out: it is the one size here that is not a --space step, and the
   steps are DESIGN.md's list to extend, not this partial's. No width or height:
   the file's 200 x 197 are the markup's, so the browser keeps the room for it
   before it loads. No media query either, so the gap does not change with the
   screen. tests/core/css/login.test.mjs holds all of this. */
.login img{
  display:block;
  margin:0 auto 24px;
}

/* A refused sign-in's message (<p class="field__error" role="alert">) is the
   first thing in the form, so it sits between the logo and the username label
   and is as wide as the form, which is as wide as the card's padding box. Its
   colour and size are 82-forms.css's .field__error; what is here is only that it
   wraps within the card.

   Ordinary text already does: the messages break at a space, or between Chinese
   characters. A token wider than the card does not -- a code, an address, a
   translation with a long compound -- and would run out past the card's right
   edge, and past the screen's on a phone. overflow-wrap:anywhere breaks it at the
   edge instead. It is `anywhere`, not `break-word`, so the form's min-content
   width does not grow with the token either. text-wrap:pretty keeps the last line
   from being one word: the English lock message is three lines at 320px, and its
   last was "min." alone.

   No white-space, overflow, text-overflow or width here, on purpose: a refusal
   that is clipped or cut with an ellipsis is a refusal the user cannot read.
   tests/core/css/login.test.mjs holds that too. */
.login .field__error{
  overflow-wrap:anywhere;
  text-wrap:pretty;
}
