/* The frame: the bar, the menu, the hub switch, and the sheet the screen lies on.

   Written on 18 September 2026 (the design refresh; Meng: "I want to feel GOOD
   when I open up this website"). Every rule for the frame lives here; before
   this they sat in six places in app.css, written for a coloured ribbon. The
   tokens are app.css's, which is loaded first. What draws the menu and the hub
   switch is public/app/shell.js; what they promise is tools/gates/shell_test.py.

   THE IDEA. DESIGN.md says the chrome is neutral and the numbers get the ink.
   The ribbon, the filled menu headings and seven colours of stripe put about a
   quarter of every screen in the hub's colour, so the chrome was the loudest
   thing on the page. Here the bar and the menu sit on --shell, a quiet desk,
   and the screen lies on it as a sheet of paper (--bg, a hairline, one rounded
   corner). The hub's colour stays where it guides and nowhere else: the hub's
   mark, the screen you are on, table headers, charts.

   Nothing here may use a raw colour, size or shadow (tools/gates/token_lint). */

/* -------------------------------------------------------------- the grid -- */
/* The menu runs the full height on the left, with the hub switch at its head;
   the bar sits over the sheet only. The markup is still header, then .shell
   holding the menu and .main -- .shell is display: contents so its two
   children are laid out by #app's grid, and the first column is `auto` so it
   simply follows the menu's own width, folded (0) or not. */
#app { --rail-w: 244px; --bar-h: 48px;
  display: grid; grid-template-columns: auto minmax(0, 1fr);
  grid-template-rows: var(--bar-h) minmax(0, 1fr);
  height: 100%; background: var(--shell); }
.shell { display: contents; }
#app > header { grid-column: 2; grid-row: 1; }
.sidebar { grid-column: 1; grid-row: 1 / 3; }
.main { grid-column: 2; grid-row: 2; display: flex; flex-direction: column; min-width: 0; min-height: 0; }

/* one size of icon per place: 16 in the menu, 14 beside small text, 18 in the bar */
.ic { width: 16px; height: 16px; flex: none; display: block; }
.ic.sm { width: 14px; height: 14px; }
.ic.lg { width: 18px; height: 18px; }

/* ---------------------------------------------------------------- the bar -- */
/* z-index 95 is the header's rung on DESIGN.md's ladder (menus 90 < header 95
   < scrim 99 ...): its two panels and the account menu open inside this
   stacking context, above anything the page positions. The long note that
   used to sit here in app.css explains what else assumes it; none of that
   changed. #app > header, never a bare `header`: Settings has one of its own. */
#app > header { display: flex; align-items: center; gap: var(--s-3);
  padding: 0 var(--s-5) 0 var(--s-3); min-width: 0;
  background: var(--shell); color: var(--ink); position: relative; z-index: 95; }
/* Phone only: the brand slot names the screen there (shell.js drawCrumbs). On
   a laptop the hub's name is at the head of the menu and the crumbs say the
   rest, so a second name in the bar would only repeat one of them. */
.brand { display: none; font-weight: 600; font-size: var(--t-lg); letter-spacing: -.01em;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }

/* A control in the bar is an icon on nothing, with a ground only under the
   pointer. One family: the menu button, Help, Search (phone), and the two
   status pills beside them. */
.barbtn, .navtoggle, .helpbtn { display: inline-grid; place-items: center; flex: none;
  width: 30px; height: 30px; padding: 0; border: 0; border-radius: var(--r-sm);
  background: transparent; color: var(--ink-2); cursor: pointer;
  transition: background var(--fast) ease-out, color var(--fast) ease-out; }
.barbtn:hover, .navtoggle:hover, .helpbtn:hover {
  background: color-mix(in srgb, var(--ink) 7%, transparent); color: var(--ink); }
.navtoggle .onphone { display: none; }
.jumpbtn { display: none; }

/* where you are */
.crumbs { display: flex; align-items: center; gap: var(--s-2); min-width: 0; margin-left: var(--s-2);
  font-size: var(--t-base); color: var(--muted); white-space: nowrap; overflow: hidden; }
/* every crumb but the last is a link (20 Sep 2026); it dresses as the button did */
.crumbs a, .crumbs button { border: 0; background: transparent; color: inherit; cursor: pointer;
  text-decoration: none; font: inherit;
  padding: var(--s-1) var(--s-3); margin: 0 calc(-1 * var(--s-2)); border-radius: var(--r-sm);
  transition: background var(--fast) ease-out, color var(--fast) ease-out; }
.crumbs a:hover, .crumbs button:hover { background: color-mix(in srgb, var(--ink) 7%, transparent); color: var(--ink); }
.crumbs .sep { display: grid; color: color-mix(in srgb, var(--muted) 60%, transparent); }
.crumbs .here { color: var(--ink); font-weight: 600; overflow: hidden; text-overflow: ellipsis; }

/* THE ADMINISTRATOR'S HEADER-EDITING CONTROLS (app/layout-edit.js draws them
   between the spacer and the freshness stamp). They keep the class they were
   born with on the ribbon, .hubbtn, because the hub switch no longer uses it
   and renaming it would touch a file this change has no business in. Quiet
   text buttons; Save is the one control in the bar that COMMITS, so it is the
   one filled, in --ink over --bg as .btn.primary is. */
.layoutctl { display: flex; gap: var(--s-2); align-items: center; flex: none; margin-right: var(--s-3); }
.layoutctl .hubbtn { height: 28px; padding: 0 var(--s-5); border: 1px solid var(--line);
  border-radius: var(--r-sm); background: transparent; color: var(--ink-2); cursor: pointer;
  font-size: var(--t-md); white-space: nowrap;
  transition: background var(--fast) ease-out, color var(--fast) ease-out; }
.layoutctl .hubbtn:hover { background: var(--panel); color: var(--ink); }
.layoutctl .hubbtn.on { background: var(--hub-soft); color: var(--hub-ink);
  border-color: color-mix(in srgb, var(--hub-ink) 35%, var(--line)); font-weight: 600; }
/* Where there are no headers to edit the pill stays, because its title says
   why (layout-edit.js), but it stops looking like a button nobody can press. */
.layoutctl .hubbtn:disabled { opacity: .55; cursor: default; border-color: transparent; }
.layoutctl .hubbtn:disabled:hover { background: transparent; color: var(--ink-2); }
.layoutctl .layoutsave { background: var(--ink); color: var(--bg); border-color: var(--ink); font-weight: 600; }
.layoutctl .layoutsave:hover { background: var(--ink-2); color: var(--bg); }

/* HOW FRESH. The headline is when the tables were last rebuilt; a dot in
   front says the verdict before the words are read: green for today's, amber
   for old (--warn, the colour that means "these numbers are old" everywhere),
   blue while it rebuilds (information, not a fault), grey when we cannot
   tell. The report's own name rides above it on a grid tab. */
.freshness { flex: none; text-align: right; font-size: var(--t-sm); line-height: 1.3;
  color: var(--ink-2); white-space: nowrap; padding: 0 var(--s-4); font-variant-numeric: tabular-nums; }
/* The report's workbook and sheet name rode above the stamp. On most screens
   it is the section and the screen again, 300px from the crumbs that already
   say so, and it made the bar two lines high on some screens and one on
   others. It is still the menu link's tooltip (shell.js navLink). */
.freshness .report { display: none; }
.freshness > div::before { content: ''; display: inline-block;
  width: 7px; height: 7px; margin-right: var(--s-3); border-radius: var(--r-round);
  background: var(--good); vertical-align: 1px;
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--good) 18%, transparent); }
.freshness .stale { color: var(--warn); font-weight: 600; }
.freshness > .stale::before { background: var(--warn);
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--warn) 20%, transparent); }
.freshness .rebuilding { color: var(--blue); font-weight: 600; }
.freshness > .rebuilding::before { background: var(--blue);
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--blue) 20%, transparent); }
/* "Cannot tell" and "is stale" must not look the same. One is a fact about the
   data, the other is an admission about our own reading of it. */
.freshness .unknown { color: var(--muted); font-style: italic; }
.freshness > .unknown::before { background: var(--muted); box-shadow: none; }
.freshness .bwyfresh { color: var(--muted); }
/* the dot rides on the verdict line only: the report's name and the ALAYA
   pull are said without one */
.freshness > .report::before, .freshness > .bwyfresh::before { content: none; }

/* Who else is here, and what needs you: one pill each, a panel on a click.
   The panels' own rows are content and stay in app.css. */
.presence { position: relative; display: flex; align-items: center; }
.presencebtn { display: flex; align-items: center; gap: var(--s-3); height: 28px;
  padding: 0 var(--s-4); border: 0; border-radius: var(--r-pill); background: transparent;
  color: var(--ink-2); font-size: var(--t-md); font-variant-numeric: tabular-nums; cursor: pointer;
  transition: background var(--fast) ease-out, color var(--fast) ease-out; }
.presencebtn:hover, .presencebtn[aria-expanded="true"] {
  background: color-mix(in srgb, var(--ink) 7%, transparent); color: var(--ink); }
/* an icon of people, not a second green dot beside the freshness stamp's */
.presencedot { display: none; }
/* THE BELL. Grey when nothing needs you; amber when something does, because
   these are things to do, not things that broke. */
.ntbtn { gap: var(--s-2); }
.ntbell { color: var(--muted); }
.ntbell.on { color: var(--warn); }
.ntbtn:hover .ntbell { color: var(--ink); } .ntbtn:hover .ntbell.on { color: var(--warn); }

/* Everyone's Feedback button keeps its WORD on a laptop (it is how anybody,
   whatever their access, tells us something is wrong); a phone keeps the icon. */
.fbbtn { display: inline-flex; align-items: center; gap: var(--s-3); flex: none; height: 28px;
  width: auto; padding: 0 var(--s-5); border: 1px solid var(--line); border-radius: var(--r-pill);
  background: transparent; color: var(--ink-2); font-size: var(--t-md); font-weight: 500; cursor: pointer; }
.fbbtn:hover { background: var(--panel); color: var(--ink); }

/* THE ACCOUNT BUTTON wears the person's initials in the hub's colour
   (shell.js setWho); the menu it opens says the name in full at its head.
   The menu box itself is .menu-body, a shared component, and stays in app.css. */
.whoname { margin: 0 0 var(--s-5); padding-bottom: var(--s-5); border-bottom: 1px solid var(--line);
  font-size: var(--t-base); font-weight: 600; overflow: hidden; text-overflow: ellipsis; }
.menu { position: relative; flex: none; margin-left: var(--s-2); }
.menu summary { list-style: none; cursor: pointer; display: grid; place-items: center;
  width: 28px; height: 28px; border-radius: var(--r-round); background: var(--ribbon);
  color: var(--on-ribbon); font-size: var(--t-sm); font-weight: 600; letter-spacing: .02em;
  transition: box-shadow var(--fast) ease-out; }
.menu summary::-webkit-details-marker { display: none; }
.menu summary:hover, .menu[open] summary {
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--hub-ink) 25%, transparent); }

/* ------------------------------------------------------- the hub switch -- */
/* The business on a button, its hubs as tabs beneath it. A person given one
   hub gets .famswitch.solo: a name, and nothing to press. */
.hubswitch { flex: none; padding: var(--s-4) var(--s-5) var(--s-4); }
.famswitch { display: flex; align-items: center; gap: var(--s-4); width: 100%; height: 36px;
  padding: 0 var(--s-3); border: 0; border-radius: var(--r-md); background: transparent;
  color: var(--ink); text-align: left; font: inherit;
  transition: background var(--fast) ease-out; }
button.famswitch { cursor: pointer; }
button.famswitch:hover, button.famswitch[aria-expanded="true"] {
  background: color-mix(in srgb, var(--ink) 6%, transparent); }
.famswitch .famname { flex: 1; min-width: 0; font-size: var(--t-lg); font-weight: 600;
  letter-spacing: -.01em; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.famswitch .ic { color: var(--muted); }
/* A hub's mark: its letter on its own colour (--hubc-<code>, set by shell.js
   as --c). Letters, because the Hub has no logo and must not invent one. */
.hubmark { display: inline-grid; place-items: center; flex: none; width: 24px; height: 24px;
  border-radius: var(--r-sm); background: var(--c, var(--ribbon)); color: var(--on-ribbon);
  font-size: var(--t-xs); font-weight: 700; letter-spacing: .02em;
  transition: background var(--slow) ease-out; }
/* WRAPS, and the reason is arithmetic: every link is nowrap, so it cannot
   shrink below its own text, and four of them -- Hub, Retail, Trading,
   Procurement -- overflowed a 244px rail that is overflow:hidden, cutting the
   fourth in half (3 Oct 2026). Wrapping gives each ~118px against the ~103px
   "Procurement" needs. A scroll was the other option and was worse: a
   scrollbar in a narrow rail, and siblings hidden behind a gesture nobody is
   told about. Degrades on its own -- two spaces one row, three or four two,
   five or six three. */
.spaces { display: flex; flex-wrap: wrap; gap: 1px; margin-top: var(--s-3); padding: var(--s-1);
  border-radius: var(--r-sm); background: color-mix(in srgb, var(--ink) 6%, transparent); }
.spaces a { flex: 1 1 calc(50% - 1px); display: inline-flex; align-items: center; justify-content: center;
  gap: var(--s-3); height: 24px; padding: 0 var(--s-4); border-radius: var(--r-xs);
  font-size: var(--t-md); font-weight: 500; color: var(--ink-2); text-decoration: none; white-space: nowrap;
  transition: background var(--fast) ease-out, color var(--fast) ease-out, box-shadow var(--fast) ease-out; }
.spaces a i { width: 6px; height: 6px; border-radius: var(--r-round); background: var(--c); opacity: .55;
  transition: opacity var(--fast) ease-out; }
.spaces a:hover { color: var(--ink); } .spaces a:hover i { opacity: 1; }
.spaces a[aria-current="true"] { background: var(--panel); color: var(--ink); font-weight: 600;
  box-shadow: var(--shadow-1); cursor: default; }
.spaces a[aria-current="true"] i { opacity: 1; }
/* IN THE DARK the chosen tab was --panel on a track that computes to the same
   value, one step in 255: only the weight said which hub you were in. It takes
   a lifted ground and a hairline there, and the dots stay at full strength
   (the --hubc- values are already lifted for dark). */
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .spaces a[aria-current="true"] { background: var(--line); box-shadow: inset 0 0 0 1px var(--grid); }
  :root:not([data-theme="light"]) .spaces a i { opacity: .85; }
}
:root[data-theme="dark"] .spaces a[aria-current="true"] { background: var(--line); box-shadow: inset 0 0 0 1px var(--grid); }
:root[data-theme="dark"] .spaces a i { opacity: .85; }
/* the list behind the button: a .menu-body like every other menu, one rung
   above the phone's drawer so it opens over it */
.spacemenu { z-index: 130; padding: var(--s-3); }
.spacemenu .spacegrp { padding: var(--s-3) var(--s-4) var(--s-2); font-size: var(--t-sm); color: var(--muted); }
.spacemenu .spacesep { height: 1px; margin: var(--s-3) calc(-1 * var(--s-3)); background: var(--line); }
.spacemenu .spacerow { display: flex; align-items: center; gap: var(--s-4); min-height: 32px;
  padding: var(--s-2) var(--s-4); border-radius: var(--r-sm); color: var(--ink);
  font-size: var(--t-base); text-decoration: none; }
.spacemenu .spacerow span:not(.hubmark) { flex: 1; }
.spacemenu .spacerow:hover { background: var(--row-hover); }
.spacemenu .spacerow .ic { color: var(--muted); }

/* --------------------------------------------------------------- the menu -- */
.sidebar { width: var(--rail-w); min-width: 0; display: flex; flex-direction: column;
  overflow: hidden; background: var(--shell); transition: width var(--slow) ease-out; }
.sidebar.collapsed { width: 0; }
/* ONE SEARCH. A button dressed as a field: it opens the jump box (Ctrl+K). */
.navsearch { display: flex; align-items: center; gap: var(--s-4); flex: none; height: 30px;
  margin: 0 var(--s-5) var(--s-4); padding: 0 var(--s-4); border: 1px solid var(--line);
  border-radius: var(--r-sm); background: color-mix(in srgb, var(--panel) 70%, transparent);
  color: var(--muted); font: inherit; font-size: var(--t-md); text-align: left; cursor: pointer;
  transition: background var(--fast) ease-out, border-color var(--fast) ease-out; }
.navsearch:hover { background: var(--panel); border-color: color-mix(in srgb, var(--muted) 45%, var(--line)); }
.navsearch .navsearchlabel { flex: 1; }
.navsearch kbd { display: inline-grid; place-items: center; min-width: 18px; height: 18px; padding: 0 var(--s-2);
  border: 1px solid var(--line); border-radius: var(--r-xs); background: var(--panel);
  color: var(--muted); font: 500 var(--t-xs)/1 var(--font); }

#nav { display: block; flex: 1; min-height: 0; overflow-y: auto; padding: var(--s-1) var(--s-4) var(--s-6); }
.navcap { display: flex; align-items: center; justify-content: space-between; height: 26px;
  margin-top: var(--s-5); padding: 0 var(--s-4); font-size: var(--t-sm); font-weight: 500; color: var(--muted); }
.navcap:first-child { margin-top: var(--s-1); }

/* A screen is an <a> (Ctrl-click opens it in a new browser tab); a section
   heading is a <button>, since a heading is not a destination. Written as
   `#nav a` and `#nav .navsecrow`, at id strength: the pin below is a button
   inside the link and must not inherit a row's width (16 Sep 2026). */
#nav a, #nav .navsecrow { display: flex; align-items: center; gap: var(--s-4); width: 100%;
  min-height: 28px; padding: 0 var(--s-4); border: 0; border-radius: var(--r-sm);
  background: transparent; color: var(--ink-2); font: inherit; font-size: var(--t-base);
  text-align: left; text-decoration: none; cursor: pointer; white-space: nowrap;
  transition: background var(--fast) ease-out, color var(--fast) ease-out; }
#nav a:visited { color: var(--ink-2); }
#nav a:hover, #nav .navsecrow:hover { background: color-mix(in srgb, var(--ink) 6%, transparent); color: var(--ink); }
#nav a .navlabel, #nav .navsecrow .navlabel { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; }
#nav a .ic { color: var(--muted); }
/* THE SCREEN YOU ARE ON: the hub's tint and the hub's ink. It was a filled
   block of the ribbon's colour; a tint says the same thing without shouting. */
#nav a[aria-current="true"], #nav a[aria-current="true"]:visited {
  background: var(--hub-soft); color: var(--hub-ink); font-weight: 600; }
#nav a[aria-current="true"] .ic { color: inherit; }
/* not given to you: still listed (Meng, 23 Aug), plainly not openable */
#nav a.locked, #nav a.locked:visited { color: var(--muted); }

/* .navsecrow, not .secrow: the Sections board (sections.js, settings.css) has a
   .secrow of its own, unscoped, and its wrap, hover and lit-state rules leaked
   onto the menu's headings for the hour this was called that. */
#nav .navsecrow { min-height: 30px; color: var(--ink); font-weight: 500; }
#nav .navsecrow .secicon { color: var(--ink-2); }
#nav .navsecrow .secn { color: var(--muted); font-size: var(--t-md); font-weight: 400; font-variant-numeric: tabular-nums; }
#nav .navsecrow .sectwist { width: 14px; height: 14px; color: var(--muted); transition: transform var(--slow) ease-out; }
#nav .navsecrow[aria-expanded="true"] .sectwist { transform: rotate(90deg); }
#nav .navsecrow.has-here { color: var(--hub-ink); }
#nav .navsecrow.locked, #nav .navsecrow.locked .secicon { color: var(--muted); }
/* a section's screens hang off a hairline under its icon */
.secbody { margin: var(--s-1) 0 var(--s-3) 15px; padding-left: var(--s-5); border-left: 1px solid var(--line); }
/* THE GROUPS an administrator gave a run of tabs (Tabs page). A named group
   (BWY: Act now, Records ...) is a caption with its colour as a dot; a
   numbered one (WDPL) keeps its colour as a dot on each screen and a gap where
   the group changes. They were a 3px stripe down every row in seven colours
   and a coloured rule between groups, which nobody could read without having
   been told the legend. */
.grpcap { display: flex; align-items: center; gap: var(--s-3); height: 22px; margin-top: var(--s-2);
  padding: 0 var(--s-4); font-size: var(--t-sm); font-weight: 600; letter-spacing: .04em;
  text-transform: uppercase; color: var(--muted); }
.grpcap:first-child { margin-top: 0; }
.grpcap i, #nav a .gdot { width: 6px; height: 6px; flex: none; border-radius: var(--r-round); background: var(--gc); }
/* a named group may wear an icon instead of its dot (sql/049, 22 Sep 2026):
   same slot, same ink as the caption, the group's colour on the stroke */
.grpcap .grpico { width: 14px; height: 14px; flex: none; color: var(--gc); }
#nav a .gdot { width: 5px; height: 5px; }
html[data-form="phone"] .brand span:last-child { overflow: hidden; text-overflow: ellipsis; }
.grpgap { height: var(--s-3); }

/* The pin at the far right of a row, shown while the pointer rests on it or
   the pin is set (app/pins.js). `#nav a .navpin`, never `.navpin` alone. */
#nav a .navpin { flex: none; display: inline-grid; place-items: center; width: 20px; height: 20px;
  min-height: 0; margin-right: calc(-1 * var(--s-2)); padding: 0; border: 0; border-radius: var(--r-xs);
  background: transparent; color: inherit; cursor: pointer; opacity: 0;
  transition: opacity var(--fast) ease-out, background var(--fast) ease-out; }
#nav a:hover .navpin, #nav a .navpin.on, #nav a .navpin:focus-visible { opacity: .8; }
#nav a .navpin:hover { opacity: 1; background: color-mix(in srgb, var(--ink) 8%, transparent); }
@media (hover: none) { #nav a .navpin { opacity: .5; } }

.navnotgiven { border: 0; background: transparent; color: var(--muted); font: inherit;
  font-size: var(--t-sm); padding: var(--s-1) var(--s-3); border-radius: var(--r-xs); cursor: pointer; }
.navnotgiven:hover { color: var(--ink); background: color-mix(in srgb, var(--ink) 6%, transparent); }
#nav .navmore { display: block; width: 100%; margin-top: var(--s-5); padding: var(--s-4); border: 1px solid var(--line);
  border-radius: var(--r-sm); background: transparent; color: var(--ink-2); font: inherit;
  font-size: var(--t-md); cursor: pointer; }
#navfoot { display: none; }

/* -------------------------------------------------------------- the sheet -- */
/* The screen lies on the desk: --bg, a hairline on the two sides that meet
   the frame, one rounded corner. No shadow -- a hairline, not a box. With the
   menu folded the sheet keeps a margin, so the corner still reads. */
.main { background: var(--bg); border: 1px solid var(--line); border-right: 0; border-bottom: 0;
  border-radius: var(--r-md) 0 0 0; overflow: hidden; }
.sidebar.collapsed ~ .main { margin-left: var(--s-4); }

/* WHICH DAY IS ON SCREEN. Absent on a good morning. A tinted ground and a
   dot, not a filled bar and not a side stripe: the site states status with
   ground and ink, and the chrome stays quiet. It deliberately does NOT scale
   with --z (that is the grid's zoom), and it has no animation -- a banner that
   slides in is one people learn to wait out. */
.datebanner { flex: none; display: flex; align-items: baseline; gap: var(--s-4); flex-wrap: wrap;
  padding: var(--s-4) var(--s-6); font-size: var(--t-md); background: var(--warn-bg); color: var(--ink);
  border-bottom: 1px solid var(--warn-edge); }
.datebanner::before { content: ''; align-self: center; width: 7px; height: 7px; flex: none;
  border-radius: var(--r-round); background: var(--warn); }
.datebanner b { font-weight: 600; }
.datebanner .why { color: var(--ink-2); }
.datebanner .when { margin-left: auto; color: var(--muted); font-size: var(--t-sm);
  font-variant-numeric: tabular-nums; white-space: nowrap; }
/* On a laptop the bar already says when it was last built, in amber, a few
   pixels above; saying it again at the banner's end made three tellings of one
   fact. A phone's bar has no stamp, so there the banner keeps it. */
html:not([data-form="phone"]) .datebanner .when { display: none; }
/* Rebuilding is a status, not a fault: blue, never amber. */
.datebanner.rebuilding { background: var(--blue-soft); border-bottom-color: var(--line); }
.datebanner.rebuilding::before { background: var(--blue); }
.datebanner.rebuilding b { color: var(--blue); }
.datebanner.unknown { background: var(--panel); border-bottom-color: var(--line); color: var(--ink-2); }
.datebanner.unknown::before { background: var(--muted); }
/* "A new version is ready": the quiet tone, in the hub's ink. */
.datebanner.update { background: var(--hub-soft); border-bottom-color: var(--line); }
.datebanner.update::before { background: var(--hub-ink); }
.datebanner.update .btn { margin-left: auto; align-self: center; white-space: nowrap; }

/* A screen with no toolbar (Home, CRM Home, the card screens) used to keep an
   empty 40px band under the banner, with a rule under it. An empty toolbar is
   no toolbar. */
.toolbar:empty { display: none; }

/* the foot of the sheet: how many rows, what is selected, what the keys do.
   A screen that says none of those has no foot: the empty strip cut the last
   row of every card screen. (:has is ignored by a browser that lacks it, and
   the strip simply stays.) */
.status:has(#status:empty):has(#selinfo:empty):has(.hintkey[hidden]) { display: none; }

.status { flex: none; display: flex; gap: var(--s-6); align-items: center; min-height: 26px;
  padding: 0 var(--s-6); font-size: var(--t-sm); color: var(--muted); font-variant-numeric: tabular-nums;
  border-top: 1px solid var(--line); background: var(--panel); }
.selinfo { color: var(--hub-ink); font-weight: 600; margin-left: auto; }
/* "Havi as at 16 Oct, 09:45" on a Havi tab (app/tab-stamps.js, sql/475;
   16 Oct 2026): amber when the live copy is older than it should be, as
   Outlet Stock's head says it */
#status .haviat.late { color: var(--warn); font-weight: 600; }
.hintkey { color: var(--muted); flex: none; }

/* ================================================================== phone ==
   html[data-form="phone"] (app.js applyForm). The menu becomes a drawer over
   the page, full height, with the hub switch at its head where the drawer's
   own row of hub buttons used to be; the bar keeps the menu button, the
   screen's name, Search, the bell, Feedback and the account. Presence, the
   freshness stamp (mirrored into the drawer's foot), the crumbs and the
   header-editing pill need a laptop's width and go. */
html[data-form="phone"] #app { display: flex; flex-direction: column; height: 100dvh; }
html[data-form="phone"] .shell { display: flex; flex: 1; min-height: 0; }
html[data-form="phone"] .main { flex: 1; border-left: 0; border-radius: var(--r-md) var(--r-md) 0 0; }
html[data-form="phone"] #app > header { flex: none; height: var(--bar-h); gap: var(--s-2); padding: 0 var(--s-4); }
html[data-form="phone"] #crumbs,
html[data-form="phone"] #freshness,
html[data-form="phone"] #presence,
html[data-form="phone"] #app > header .spacer,
html[data-form="phone"] .layoutctl { display: none !important; }
/* the hub's mark, then the screen's name, centred on one line */
html[data-form="phone"] .brand { display: flex; align-items: center; gap: var(--s-4); flex: 1; min-width: 0; margin-left: var(--s-3); }
html[data-form="phone"] .brand .hubmark { width: 22px; height: 22px; }
html[data-form="phone"] .navtoggle .onlaptop { display: none; }
html[data-form="phone"] .navtoggle .onphone { display: block; }
html[data-form="phone"] #jumpbtn { display: inline-grid; }
html[data-form="phone"] .fbbtn { width: 30px; padding: 0; justify-content: center; border: 0; }
html[data-form="phone"] .fbbtn .fblabel { display: none; }
html[data-form="phone"] .barbtn, html[data-form="phone"] .navtoggle,
html[data-form="phone"] .helpbtn, html[data-form="phone"] .fbbtn { width: 40px; height: 40px; }
/* the account button keeps the same floor as its neighbours */
html[data-form="phone"] #whoami { min-height: 40px; min-width: 40px; }

/* The drawer. Fixed and full height, above the bar; hidden as well as
   off-screen, so a Tab press cannot land on a link nobody can see; the shadow
   only while it is open. The laptop's fold (.collapsed) is neutralised: a
   class left over from a laptop session must not wedge the drawer shut. */
html[data-form="phone"] .sidebar, html[data-form="phone"] .sidebar.collapsed {
  position: fixed; inset: 0 auto 0 0; width: min(320px, 86vw); z-index: 97;
  transform: translateX(-102%); visibility: hidden;
  transition: transform var(--slow) ease-out, visibility 0s linear var(--slow); }
html[data-form="phone"] .sidebar.open { transform: none; visibility: visible;
  box-shadow: var(--shadow-3); transition: transform var(--slow) ease-out, visibility 0s; }
/* [hidden] still wins here: the attribute is how toggleNav shows and hides it */
html[data-form="phone"] #navscrim { position: fixed; inset: 0; z-index: 96; background: var(--scrim); }
html[data-form="phone"] .hubswitch { padding-top: calc(var(--s-5) + env(safe-area-inset-top)); }
html[data-form="phone"] .spaces a { height: 36px; font-size: var(--t-base); }
html[data-form="phone"] .famswitch { height: 44px; }
html[data-form="phone"] .navsearch { height: 40px; font-size: var(--t-base); }
html[data-form="phone"] .navsearch kbd { display: none; }
/* 44px rows: a finger, not a pointer, has to land on one */
html[data-form="phone"] #nav a, html[data-form="phone"] #nav .navsecrow { min-height: 44px; font-size: var(--t-lg); }
html[data-form="phone"] #nav a .navpin { width: 32px; height: 32px; opacity: .5; }
html[data-form="phone"] .spacemenu .spacerow { min-height: 44px; font-size: var(--t-lg); }
/* The freshness line, mirrored here by drawFreshness because the bar has no
   room for it: the bare label, in the page's own tokens. */
html[data-form="phone"] #navfoot { display: block; flex: none;
  padding: var(--s-4) var(--s-5) calc(var(--s-4) + env(safe-area-inset-bottom));
  font-size: var(--t-sm); color: var(--muted); border-top: 1px solid var(--line); }
html[data-form="phone"] #navfoot .stale { color: var(--warn); font-weight: 600; }
html[data-form="phone"] #navfoot .rebuilding { color: var(--blue); font-weight: 600; }
html[data-form="phone"] #navfoot .unknown { font-style: italic; }
