/* ═══════════════════════════════════════════════════════════════════════════
   Platinumlist design system — primitives and semantic tokens.

   There is no shared design-system.css package on this machine; the other
   ported dashboards inline their tokens, which is how ~30 rgba() literals of
   an old palette ended up hardcoded in one of them. This file exists so both
   pages of this project share ONE definition.

   Themes use CSS light-dark(). It resolves against `color-scheme`, so the
   theme switch is a color-scheme switch:

     :root                    color-scheme: light   (default — light loads first)
     [data-theme="light"]     color-scheme: light
     [data-theme="dark"]      color-scheme: dark

   Every semantic token below is therefore written once, not twice.
   ═══════════════════════════════════════════════════════════════════════════ */

@font-face {
  /* Bold is the ONLY licensed weight — never request another.
     Regular is not on this machine and preloading it 404s. */
  font-family: 'MD Nichrome';
  src: url('/fonts/MDNichrome-Bold.otf') format('opentype');
  font-weight: 700;
  font-style: normal;
  font-display: swap;
}

:root {
  color-scheme: light;

  /* ── Primitives ─────────────────────────────────────────────────────────
     Aussie is the neutral ramp, Poodle the brand purple. Named, not guessed:
     these are the values the rest of the estate already uses. */
  --aussie-100: #0F0F15;
  --aussie-95:  #18181E;
  --aussie-90:  #24242E;
  --aussie-80:  #2C2C38;
  --aussie-60:  #6E6E81;
  --aussie-40:  #9999AC;
  --aussie-30:  #B3B3C3;
  --aussie-20:  #ECEDF2;
  --aussie-10:  #F5F5F8;
  --aussie-0:   #FFFFFF;

  --poodle-primary: #7E05E8;
  --poodle-60:      #9737EC;
  --poodle-40:      #B169F1;
  --poodle-20:      #D8B4F8;

  --accent-success:  #30BB47;
  --accent-alert:    #F93643;
  --accent-warning:  #FF8300;   /* orange. NOT caution-yellow: it fails on white */
  --accent-caution:  #F9C54B;
  --accent-discount: #E8467C;
  --link-base:       #1A56FF;

  /* An accent has TWO jobs and they do not want the same value. A colour that
     is right as a 3px live-feed dot, a 1px border or a 10% fill is not right
     as 9px text sitting on that same fill — a dot is judged by whether you can
     see it at all, text by whether you can read the shape of every letter, and
     WCAG prices those differently (3:1 against 4.5:1).
     `--accent-success` is the FILL strength and stays exactly as it is: the
     dot, the `.is-real` border and the 10% chip ground are all correct at
     #30BB47. `--accent-success-ink` is the TEXT strength of the same colour
     family, and it is deliberately a different value — same hue, taken down
     until it clears 4.5:1 on the ground the text actually lands on.

     Measured (the ratios, not the eye — this whole class of bug exists because
     somebody looked at it and it seemed fine):

       #30BB47 on the 10% chip over a white card (#EAF8ED)  2.30   FAIL
       #1B6B37 on the 10% chip over a white card (#EAF8ED)  5.97   pass
       #1B6B37 on the 10% chip over the grey page (#D9E8E1) 5.17   pass
       #1B6B37 on plain white 6.55, on the grey page ground 5.60   pass

     Dark side: #30BB47 already clears 4.5 there (4.52–6.67 across the 10–18%
     tints over #0F0F15/#18181E/#24242E), so the dark ink is comfort rather
     than rescue — #6FD394 measures 6.20 at the worst of those tints and 9.16
     at the best. Use the ink token for text on both themes anyway, so a page
     never has to know which theme it is in to pick the right one.

     Red and amber have the SAME split and no ink token yet, because nothing in
     this file sets them as text — the failing rules are on the pages. For
     whoever adds them: #F93643 is 3.73 on white and 3.25 on its own 10% tint,
     #FF8300 is 2.47 and 2.26, and the lightest passing inks of those hues are
     about #A63F46 and #815D37. */
  --accent-success-ink: light-dark(#1B6B37, #6FD394);

  /* ── Semantic surfaces ──────────────────────────────────────────────────
     The page is grey and cards are white, never both white — a card has to be
     visible against the ground it sits on. */
  --bg-default:      light-dark(#FFFFFF, #0F0F15);
  --bg-secondary:    light-dark(#ECEDF2, #24242E);
  --bg-hover:        light-dark(#F5F5F8, #2C2C38);
  --bg-wash:         light-dark(#F5F5F8, #18181E);
  --bg-border:       light-dark(#DFE0E8, #34343F);
  --bg-border-hover: light-dark(#C4C5D2, #45454F);
  --bg-fade:         light-dark(rgba(15,15,21,0.45), rgba(0,0,0,0.62));

  /* ── Semantic content ───────────────────────────────────────────────────
     Three levels, and they are not interchangeable:
       primary    text you read
       secondary  labels, metadata, column headers — still read
       highlight  placeholder and disabled ONLY — decorative
     Mapping column headers to `highlight` renders them at #B3B3C3 and they
     effectively vanish.

     `--content-secondary` is read on TWO grounds and has to clear 4.5:1 on
     both. That is the whole story of its value. It was picked against white
     (#6E6E81 is 4.99 there, which passes) — but the app's page ground is
     `--bg-secondary` #ECEDF2, not white, and that is where `.note`,
     `.section-title`, `.footnote`, `#roundLabel` and the "Made with" pill
     actually sit. On the grey it measured 4.27 and failed. #66667A is 4.79 on
     the grey and 5.61 on white, so it passes on both. Check any change to this
     token against the GREY first: white is the surface it is easiest to pass
     on and the one this token is least often read on. Same class of error the
     comment above records for `--content-highlight` — a value validated
     against the wrong ground. The dark value is untouched (7.97 on #0F0F15,
     6.41 on #24242E). */
  --content-primary:   light-dark(#0F0F15, #FFFFFF);
  --content-secondary: light-dark(#66667A, #A6A6B8);
  --content-tertiary:  light-dark(#9999AC, #86869A);
  --content-highlight: light-dark(#B3B3C3, #6E6E81);
  --content-inverse:   light-dark(#FFFFFF, #0F0F15);

  /* ── Type ───────────────────────────────────────────────────────────────
     MD Nichrome is a DISPLAY face: headings and metric values only. Body,
     tables and labels stay on the UI face. */
  --font: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
  --font-accent: 'MD Nichrome', 'Inter', sans-serif;
  --font-mono: 'JetBrains Mono', ui-monospace, SFMono-Regular, Menlo, monospace;
  --font-family-default: var(--font);

  /* ── The system's own furniture ─────────────────────────────────────────
     Every page sets `viewport-fit=cover` and iOS is told the status bar is
     `black-translucent`, which together mean the page is drawn UNDERNEATH the
     notch and the home indicator. Nothing consumed the TOP inset until now, so
     an installed iPhone drew the clock and the battery straight over the
     Platinumlist logo. Read through variables rather than calling env()
     inline, so a test can override them — a browser at 375x812 reports 0 for
     both and would show the bug as fixed whether it is or not. */
  --safe-top:    env(safe-area-inset-top, 0px);
  --safe-bottom: env(safe-area-inset-bottom, 0px);
  /* The left one matters now that the desktop nav is a rail against that edge.
     An iPad in landscape puts the home indicator's inset there, and a notched
     phone rotated left puts the notch there. */
  --safe-left:   env(safe-area-inset-left, 0px);

  /* The space above the header. It belongs to the BAR, not to `.app` — see the
     locked-header section. Two values because a phone gets less of it. */
  --app-pad-top: 28px;

  /* ── Shape, depth, motion ───────────────────────────────────────────────*/
  --radius-xs:   8px;
  --radius-sm:   10px;
  --radius-base: 14px;
  --radius-lg:   20px;

  /* The light-dark() goes around the COLOUR, not around the whole shadow.
     `light-dark()` is a colour function, so `light-dark(0 1px 2px rgba(...),
     ...)` does not parse — and a custom property is only validated where it is
     used, so nothing complained: the tokens looked fine and every
     `box-shadow: var(--shadow-*)` in the estate silently computed to `none`.
     The floating nav pill on a phone asks for --shadow-lg and had no shadow at
     all; so did the login card, the round menu and the alert drawer. Verified
     with CSS.supports: `color` accepts light-dark(), `box-shadow` refuses it.
     (2026-09-07.) */
  --shadow-sm: 0 1px 2px light-dark(rgba(15,15,21,0.06), rgba(0,0,0,0.4));
  --shadow-md: 0 4px 16px light-dark(rgba(15,15,21,0.10), rgba(0,0,0,0.5));
  --shadow-lg: 0 12px 32px light-dark(rgba(15,15,21,0.14), rgba(0,0,0,0.6));

  --duration-base: 160ms;
}

/* An explicit choice beats the OS. Both directions are declared so the toggle
   wins whichever way the system is set. */
[data-theme="light"] { color-scheme: light; }
[data-theme="dark"]  { color-scheme: dark; }

/* ═══════════════════════════════════════════════════════════════════════════
   Skip link — the way past the nav.

   `<a class="pl-skip" href="#content">Skip to content</a>` as the FIRST child
   of <body> on every page. Both pages put five nav items ahead of the content,
   so a keyboard or switch user paid five tabs on every navigation to reach the
   thing they came for, and a screen-reader user heard the same five links read
   out again on each page. One rule here rather than one per page, for the same
   reason the tab bar lives here: two copies drift.

   Hidden with a 1px clip and NOT with `display:none` or `visibility:hidden`.
   Both of those take the link out of the focus order, which turns a skip link
   into dead markup that only a source view can find — the classic way to ship
   this control and have it do nothing. Clipping leaves it focusable, and the
   first Tab on the page brings it back to full size.

   z-index 100. The ladder in this file is nav rail 45, sticky header 30,
   modal 50, round menu 60, alert drawer 90/91, and the skip link has to clear
   ALL of them: it is drawn at the top-left of the viewport, which is exactly
   where the header and the desktop rail are, and a focused control you cannot
   see is the bug this is fixing. It is on screen only while focused, so
   nothing else is ever underneath it.

   `position: fixed` so it costs no layout, and the safe-area insets are added
   when it appears — the top-left corner of a notched phone in landscape is
   under the notch, and this file already reads both insets through variables
   so a test can override them.

   White on --poodle-primary measures 6.83:1. 16px is the text floor iOS wants
   on anything focusable (see the pointer:coarse block at the foot of this
   file), and the 44px min-height is the touch floor — a skip link is usually
   reached by keyboard but it is a real control and sizing it like one costs
   nothing.
   ═══════════════════════════════════════════════════════════════════════════ */
.pl-skip{
  position: fixed;
  top: 0;
  left: 0;
  z-index: 100;
  width: 1px;
  height: 1px;
  padding: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}
.pl-skip:focus{
  width: auto;
  height: auto;
  overflow: visible;
  clip-path: none;

  /* Stated rather than inherited. The pages happen to set
     `*{box-sizing:border-box}` in their own <style>, but this file has no such
     rule of its own, and under the default content-box `min-height` is a floor
     on the CONTENT and the 10px padding is added on top of it — measured that
     way the control came out 64px tall rather than 44. One word here and the
     44 is the number it reads as, whatever the page around it does. */
  box-sizing: border-box;
  min-height: 44px;

  /* Offset from the corner with margin rather than top/left, so the hidden
     state above keeps a plain 0,0 origin and there is only one place the
     insets are applied. */
  margin: calc(var(--safe-top) + 8px) 0 0 calc(var(--safe-left) + 8px);

  display: flex;
  align-items: center;
  padding: 10px 18px;
  border-radius: var(--radius-sm);
  background: var(--poodle-primary);
  color: #FFFFFF;
  font-family: var(--font);
  font-size: 16px;
  font-weight: 600;
  line-height: 1.2;
  text-decoration: none;
  box-shadow: var(--shadow-lg);

  /* The ring flips with the theme (`--content-primary` is near-black on light
     and white on dark), so the purple block is outlined against whichever
     ground the page is painting — a purple-on-purple focus ring is the one
     thing this control cannot afford. */
  outline: 2px solid var(--content-primary);
  outline-offset: 2px;
}

/* `.pl-skip:focus{display:flex}` is an author `display`, so it must carry the
   `[hidden]` escape — the rule this project has now paid for four times
   (.approx-tag, .sub-tabs, .pl-tabs a, .mw-lockup img). Belt and braces here,
   since a `[hidden]` element is not focusable and the rule above therefore
   cannot match one; the point is that the pattern is never left incomplete for
   the next person to copy. */
.pl-skip[hidden]{ display: none; }

/* ═══════════════════════════════════════════════════════════════════════════
   Tab bar — icon over label in a pill, the active one filled.

   Defined here rather than in each page so the three pages cannot drift apart;
   a nav that looks slightly different on every screen reads as three separate
   tools rather than one. Mark the current page with aria-current="page" — the
   styling hangs off that attribute, so the highlight and the accessible state
   can never disagree.
   ═══════════════════════════════════════════════════════════════════════════ */
/* `gap:2px` here is never what ships. Both breakpoints below override it to
   6px — the desktop rail always did, and the phone pill now does too (see the
   mobile block for why 2px was wrong). The two queries tile every width, so
   this value is only ever the starting point. */
.pl-tabs{
  display:inline-flex; align-items:center; gap:2px; padding:5px;
  background: var(--bg-default);
  border:1px solid var(--bg-border);
  border-radius:999px;
  box-shadow: var(--shadow-sm);
}
.pl-tabs a{
  display:flex; flex-direction:column; align-items:center; gap:3px;
  min-width:66px; padding:6px 14px 5px;
  border-radius:999px; text-decoration:none;
  font-family: var(--font); font-size:11px; font-weight:500; line-height:1;
  color: var(--content-secondary);
  transition: color var(--duration-base);
  -webkit-tap-highlight-color: transparent;
}
.pl-tabs a .ic{
  display:flex; align-items:center; justify-content:center;
  width:32px; height:24px; border-radius:9px;
  transition: background var(--duration-base);
}
.pl-tabs a svg{
  width:17px; height:17px; display:block;
  fill:none; stroke:currentColor; stroke-width:1.7;
  stroke-linecap:round; stroke-linejoin:round;
}
.pl-tabs a:hover{ color: var(--content-primary); }
.pl-tabs a:focus-visible{ outline:2px solid var(--poodle-primary); outline-offset:2px; }

/* `.pl-tabs a{display:flex}` above is an author rule and therefore BEATS the
   browser's `[hidden]{display:none}`. Without this line the role-gated items
   (People, Roles, Activity) stay laid out for everybody even though nav.js has
   marked them hidden: on a phone that made the bottom pill five items wide —
   clamped to `calc(100vw - 16px)`, sprawling across the card underneath — for a
   person who may open only two of them, and `sizeToFit()` never applied
   `is-wide` because it correctly counted two. It also showed tabs that answer
   403. Same trap as `.sub-tabs[hidden]` and `.approx-tag[hidden]`: if a rule
   here sets `display`, it must opt back out of `[hidden]`. */
.pl-tabs a[hidden]{ display:none; }

/* Held by nav.js for the one fetch on a browser with no cached tab list, so the
   pill fades in already at its final width rather than painting narrow and
   growing. nav.js reveals it on the answer, on a failure, or after a timeout,
   so this class is never a state the pill can get stuck in. */
.pl-tabs{ transition: opacity var(--duration-base) ease; }
.pl-tabs.is-pending{ opacity:0; visibility:hidden; }
@media (prefers-reduced-motion: reduce){
  .pl-tabs{ transition:none; }
}

.pl-tabs a[aria-current="page"]{ color: var(--poodle-primary); font-weight:600; }
.pl-tabs a[aria-current="page"] .ic{ background: rgb(126 5 232 / 0.12); }

/* Poodle at full strength is too heavy on a dark ground — same paired value
   the rest of the app uses. */
[data-theme="dark"] .pl-tabs a[aria-current="page"]{ color: var(--poodle-40); }
[data-theme="dark"] .pl-tabs a[aria-current="page"] .ic{ background: rgb(177 105 241 / 0.18); }

@media (max-width: 560px){
  .pl-tabs a{ min-width:56px; padding:6px 9px 5px; font-size:10px; }
}

/* ── Desktop: the tabs are a floating icon rail down the left ───────────────
   Arif's call, 2026-09-08, to a reference screenshot: a narrow rounded card
   held off the left edge, the Platinumlist mark at the top, icons only.
   It used to be the same horizontal pill the phone gets, in the top-right of
   the header.
 *
 * The nav is `position: fixed` at BOTH breakpoints — a bottom pill on a phone,
 * this rail on a desktop. One mechanism, two edges, and it is out of flow from
 * wherever its markup happens to sit.
 *
 * This comment used to say the markup was deliberately NOT moved, on the
 * grounds that where an out-of-flow element sits in the DOM is invisible. That
 * was wrong in one way, and the 8 Sept audit found it: DOM order is what
 * decides FOCUS order. With the nav inside the header's control group, tabbing
 * went round picker (centre) → rail (far left) → back up to the controls (top
 * right). So on 2026-09-08 the `<nav class="pl-tabs">` was moved in both pages
 * to be the first child of `.app`, ahead of `<header class="topbar">`. Nothing
 * moved visually, because `position: fixed` does not care; the reading order
 * now matches the screen.
 *
 * Two things that made the move safe, both checked first: no CSS anywhere
 * depends on a `.topbar-controls .pl-tabs` or `.topright .pl-tabs` descendant
 * relationship, and `nav.js` finds the element by class. It is still markup in
 * each page rather than injected by script, so it paints before anything runs
 * and a page with no session still has a nav — that part of the original
 * reasoning stands and should not be undone.
 *
 * The page is inset with `padding-left` on the BODY rather than on `.app`,
 * because `.app` is `max-width:1180px; margin:0 auto` — padding it would shift
 * the content off centre inside its own box, while padding the body leaves it
 * centred in what is left.
 *
 * z-index 45 puts the rail ABOVE the sticky header (30) — the header's
 * background bleeds `-100vw` to either side and would otherwise paint over the
 * rail — and below the modal (50), the round menu (60) and the alert drawer
 * (90/91). A nav on top of a dialog is a nav you cannot dismiss. */
@media (min-width: 601px){
  /* The card, and the gutter it floats in: 68 + 14 either side. */
  :root{ --nav-w: 68px; --nav-gap: 14px; }

  html body{ padding-left: calc(var(--nav-w) + var(--nav-gap) * 2 + var(--safe-left)); }

  .pl-tabs{
    position: fixed;
    top: calc(var(--safe-top) + var(--nav-gap));
    bottom: var(--nav-gap);
    left: calc(var(--nav-gap) + var(--safe-left));
    width: var(--nav-w);
    z-index: 45;

    flex-direction: column;
    align-items: center;
    justify-content: flex-start;
    gap: 6px;
    padding: 14px 0;

    border-radius: 30px;
    box-shadow: var(--shadow-lg);
    /* NOT `auto`. The label tooltips are positioned outside the card, and an
       overflow of any kind on this element clips them. Five items at 44px plus
       the mark is about 330px, so the only way to overflow it is a desktop
       window under ~360px tall, which the mobile breakpoint does not catch
       because it keys on width. Accepted: the last item clips rather than the
       tooltips never showing at all. */
    overflow: visible;
  }

  /* The mark, at the top of the rail. A pseudo-element rather than markup, so
     neither page has to carry it and the two cannot drift. Two files and not
     one recolourable SVG for the usual reason — both are outlined paths with a
     baked `fill`, so the theme picks the FILE. */
  .pl-tabs::before{
    content: '';
    flex: none;
    width: 40px; height: 40px;
    margin-bottom: 10px;
    border-radius: 13px;
    background: var(--bg-secondary) url('/assets/icon-purple.svg') center / 21px 21px no-repeat;
  }
  [data-theme="dark"] .pl-tabs::before{
    background-image: url('/assets/icon-white.svg');
  }

  .pl-tabs a{
    position: relative;
    flex: none;
    width: 44px; height: 44px;
    padding: 0;
    gap: 0;
    justify-content: center;
    border-radius: 14px;
    transition: background var(--duration-base), color var(--duration-base);
  }
  .pl-tabs a .ic{ width: auto; height: auto; background: none; }
  .pl-tabs a svg{ width: 20px; height: 20px; }
  .pl-tabs a:hover{ background: var(--bg-hover); }

  /* Icon-only, so the label becomes a tooltip rather than being deleted. It
     stays in the DOM and in the accessibility tree — `display:none` or an
     empty anchor would leave five links a screen reader announces only by
     their href, and there is no `title` in the markup to fall back on. */
  .pl-tabs a .lbl{
    position: absolute;
    left: calc(100% + 10px);
    top: 50%;
    transform: translateY(-50%);
    padding: 5px 10px;
    border-radius: 8px;
    background: var(--content-primary);
    color: var(--bg-default);
    font-size: 12px;
    line-height: 1;
    white-space: nowrap;
    pointer-events: none;
    opacity: 0;
    transition: opacity var(--duration-base);
    z-index: 1;
  }
  .pl-tabs a:hover .lbl,
  .pl-tabs a:focus-visible .lbl{ opacity: 1; }
  @media (prefers-reduced-motion: reduce){
    .pl-tabs a .lbl{ transition: none; }
  }

  /* The whole square lights, not a chip inside it — at 44px the icon's own
     background WAS the square, so the two would sit one inside the other. */
  .pl-tabs a[aria-current="page"]{ background: rgb(126 5 232 / 0.11); }
  [data-theme="dark"] .pl-tabs a[aria-current="page"]{ background: rgb(177 105 241 / 0.18); }

  /* `is-pending` exists to stop the PILL painting narrow and growing when the
     session reveals the gated items. A fixed-width rail cannot do that: the
     revealed items are the last three, so they extend the list downwards and
     nothing already drawn moves. Hiding the rail for that fetch would be a
     blank column for up to 1.2s and no way to navigate, which is the failure
     it was written to avoid. So it does not apply here. */
  .pl-tabs.is-pending{ opacity: 1; visibility: visible; }
}

/* ── Mobile: the tabs move to the bottom ────────────────────────────────────
   Thumbs reach the bottom of a phone; they do not comfortably reach a nav
   stacked under a header. Fixed to the bottom as a floating pill, centred,
   clear of the home indicator.

   z-index 40 is deliberate and sits BELOW the modal (50) and the alert drawer
   (90/91) — a nav floating on top of a dialog is a nav you cannot dismiss. */
@media (max-width: 600px){
  :root{ --app-pad-top: 18px; }

  .pl-tabs{
    position: fixed;
    left: 50%;
    transform: translateX(-50%);
    /* A CONSTANT, deliberately — no `env(safe-area-inset-bottom)` in here.
     *
     * Measured on Arif's iPhone, 2026-09-08, with the ?navdebug readout: the
     * same build reported the inset as 34px on Settings and 0px on the
     * Overview, so the pill rested 44px up on one page and 10px up on the
     * other. Nothing in this project differs between those two pages — one
     * definition of the token, no per-page rule on .pl-tabs, `viewport-fit=cover`
     * on both. Safari decides that value and it is not stable page to page, so
     * anything built on it inherits the wobble. Three attempts to correct for
     * it in JS failed for that reason.
     *
     * 34px is the height of the home indicator, which is what the inset is for.
     * The trade, stated plainly: on a device that has no home indicator the
     * pill sits about 24px higher than it strictly needs to. That is the price
     * of it being in the same place on every page, which is what was asked for
     * three times.
     *
     * There is no JavaScript in this value either. `--nav-lift` used to be
     * added here, raising the pill when a browser toolbar occluded the bottom;
     * it was correct in Chromium and WebKit and still wrong on the device,
     * because Safari's visual-viewport height varies with scroll state and
     * scroll state varies with whether the page has data. It was the fourth
     * thing to move this pill, so it is gone. A literal cannot vary. */
    bottom: 34px;
    z-index: 40;
    box-shadow: var(--shadow-lg);
    padding: 6px;

    /* 6px, not the base rule's 2px. Two 44px+ targets 2px apart do not behave
     * as two targets: the thumb lands in the seam and the browser awards the
     * tap to whichever side won by a pixel. This is the most-tapped control in
     * the product and it is worked one-handed, at a turnstile, in the dark, so
     * a coin-flip between Overview and Settings is not an acceptable outcome.
     * The desktop rail has always used 6px; the phone — the only one operated
     * by a thumb — was the tighter of the two, which is backwards.
     *
     * The 16px this adds across a five-item pill comes back out of the items'
     * own width below, so the pill does not grow — it is capped at
     * `max-width: calc(100vw - 16px)` and nav.test.js pins both ends of that.
     * Measured on the real pages at 375px, five items, `is-wide`:
     *
     *     before   304.1px   2px gaps   items 58.1, 56, 56, 56, 56
     *     after    300.1px   6px gaps   items 54.1, 52, 52, 52, 52
     *
     * and the two-item Viewer pill goes 162.7 -> 164.0, the safe side of the
     * 220px the same test asserts. Every item is still over 50px wide and
     * 50px tall, well clear of the 44px touch floor. */
    gap: 6px;
  }
  /* Measure this on a PAGE, not on the stylesheet alone. Both pages set
     `*{box-sizing:border-box}` in their own <style>, so `min-width` here is a
     floor on the item's OUTER width and the padding comes out of the inside of
     it — sized in a bare document (content-box) every number above is wrong by
     the padding, which is how a first pass at this landed on a pill 14px wider
     than it measured. The floor also does double duty as the flex SHRINK
     floor, which is what the 320px case turns on; see `is-wide` below.
     72px against "Overview" at 50.7px (11px Inter) leaves the item padding-led
     rather than label-led, so all three ungated widths stay uniform. */
  .pl-tabs a{ min-width: 72px; padding: 7px 10px 6px; font-size: 11px; }
  .pl-tabs a .ic{ height: 26px; }

  /* Four or five items do not fit a 375px phone at 72px each — 5 x 72 plus
     padding is wider than the screen, and the pill would sit edge to edge or
     clip. `is-wide` is set by nav.js from the count of items this person can
     actually see, so a Viewer with two keeps the roomier pill and only a role
     that really has five pays for them. Never let it exceed the viewport. */
  .pl-tabs{ max-width: calc(100vw - 16px); }
  /* 52, down from 56, and 4px of padding, down from 6 — that is where the
     16px of new gap is paid for. 52 is not an arbitrary trim: it is the
     smallest floor that still sizes every item, because the widest label at
     10px Inter is "Overview" at 46.1px and 46.1 + 8 = 54.1. Go below 52 and
     the narrow items stop being floored and start being label-led — "Roles"
     collapses to 34px, which is under the touch floor and makes a pill of five
     different widths. Measured: 52 gives 54.1, 52, 52, 52, 52.

     It is also the flex SHRINK floor, and that is what makes 320px work. The
     pill is capped at `calc(100vw - 16px)` = 304px there, so five items have
     to fit inside 304 - 24 gaps - 14 padding-and-border = 266. At a 56px floor
     they cannot shrink below 280 and 13px of the last item hangs out past the
     rounded end; at 52 the floor is 260 and it fits. Measured on a 320px
     viewport, both ways. A 375px phone never reaches the cap, so this is
     invisible until somebody opens it on the small one. */
  .pl-tabs.is-wide a{ min-width: 52px; padding: 7px 4px 6px; font-size: 10px; }
  .pl-tabs.is-wide a .ic{ width: 28px; height: 24px; }

  /* The bar floats over the page, so the page has to end above it or the last
     element is unreachable — the footnote was sitting 41px behind it.
     `html body` and not `body`: every page sets `html, body{ padding:0 }` in
     its own <style>, which loads after this file and would otherwise win on
     equal specificity. */
  html body{ padding-bottom: calc(96px + var(--safe-bottom)); }

  /* iOS pins a position:fixed element to the VISUAL viewport, so when the
     keyboard opens the pill rides up with it and comes to rest on top of the
     form underneath — in the role editor it sat squarely over Save / Cancel /
     Delete role. Nobody needs the section nav while they are typing, so it
     stands down until the field is blurred (class set by nav.js). */
  .pl-tabs.is-typing{ display: none; }
}

/* ── "Made with Platinumlist AI" ────────────────────────────────────────────
   The attribution badge. Two <img> and not one recolourable SVG for the same
   reason `.brand` carries two: both logo files are outlined paths with a baked
   `fill`, so the theme is chosen by which file is SHOWN, never by
   `currentColor`.

   The `AI` is live text in the display face rather than part of the artwork.
   That keeps the badge on the two logo files every page already caches, and it
   scales with the text instead of pixelating.

   The badge is named ONCE, on the pill, as `role="img"` with an aria-label,
   and every piece inside it is aria-hidden. Built the obvious way instead —
   alt="Platinumlist" on the logo plus a live "AI" span — the accessibility tree
   came out as "Made with Platinumlist" with the AI dropped entirely, and BOTH
   images listed, because the alt of a display:none image is not reliably
   pruned. One name on the container has neither problem.

   Note there is deliberately NO `.mw-lockup img{ display:block }`. That is the
   trap this project has now paid for three times (.approx-tag, .sub-tabs,
   .pl-tabs a): a bare `display` on the element beats the `display:none` that
   hides the wrong theme's logo, and BOTH would render. Sizing is set on its own
   rule that touches no display, and every display lives on a `.light-only` /
   `.dark-only` selector at matching specificity. */
.pl-madewith{
  display: flex;
  justify-content: center;
  padding: 30px 16px 6px;
}
.pl-madewith .mw-pill{
  display: inline-flex;
  align-items: center;
  gap: 9px;
  padding: 7px 15px;
  border-radius: 999px;
  background: var(--bg-secondary);
  border: 1px solid var(--bg-border);
  font-family: var(--font);
  font-size: 13px;
  line-height: 1;
  color: var(--content-secondary);
  /* Not a link and not a control. On a phone it sits just above the floating
     nav pill, and a stray selection drag there should not highlight it. */
  user-select: none;
  -webkit-user-select: none;
}
.pl-madewith .mw-lockup{ display: inline-flex; align-items: flex-start; gap: 1.5px; }
.pl-madewith .mw-lockup img{ height: 15px; width: auto; }

.pl-madewith .mw-lockup img.light-only{ display: block; }
.pl-madewith .mw-lockup img.dark-only{ display: none; }
[data-theme="dark"] .pl-madewith .mw-lockup img.light-only{ display: none; }
[data-theme="dark"] .pl-madewith .mw-lockup img.dark-only{ display: block; }

.pl-madewith .mw-ai{
  font-family: var(--font-accent);
  font-size: 8px;
  line-height: 1;
  margin-top: 1px;
  letter-spacing: 0.02em;
  /* Follows the wordmark it hangs off: purple on light, white on dark. The
     brand purple on the dark surface would be all but unreadable at 8px. */
  color: light-dark(var(--poodle-primary), var(--aussie-0));
}

/* ── The locked header ──────────────────────────────────────────────────────
   Both pages put `.topbar` inside `.app`, so one rule locks both, and it locks
   at EVERY width. Nothing is hidden or shrunk at any size: the whole header
   stays.

   This used to add "on desktop the section nav lives inside the bar, so making
   the bar sticky is what pins the nav too". That has not been true since
   2026-09-08: `.pl-tabs` is `position: fixed` at every width and its markup is
   now the first child of `.app`, ahead of the header, so the two are pinned
   independently. The bar being sticky pins the HEADER; the nav pins itself.

   `padding-top: var(--safe-top)` sits on the bar itself and not on `.app`,
   because the bar is the thing that overlaps the status bar. Put the inset on
   the container and it scrolls away with the content, and the collision comes
   straight back the moment you move. It applies at all widths because
   `viewport-fit=cover` does — an iPad in standalone has a status bar too.

   What it costs, measured, and accepted: the full bar plus the nav pill take
   29-32% of a phone once the notch and home indicator are counted, so a 375px
   screen shows three match cards where it used to show four. Arif's call,
   2026-09-07, after seeing the numbers — a header you can always reach beats
   a fourth card you have to scroll to.

   The background is a pseudo-element bleeding to either side rather than a
   background on the bar, because `.app` pads itself by 14px on a phone and
   24px above that, and a background on the bar alone would leave two unpainted
   gutters for the cards to show through as they pass underneath.

   That bleed needs the page to clip horizontally, and the page already did —
   with `overflow-x: hidden`, which is exactly what stopped the bar sticking.
   `hidden` on one axis forces the other to `auto`, so html and body both
   became scroll containers and a sticky descendant has nothing left to stick
   to; measured, the bar scrolled 682px off the top. `clip` clips identically
   WITHOUT creating a scroll container. It is behind @supports because Safari
   only took it in 16: an older iPhone keeps `hidden`, so the bleed is still
   clipped and the header simply does not lock — which is what it did before
   today, not a regression. */
@supports (overflow-x: clip){
  /* `:root` and `html body` rather than `html, body`: every page re-declares
     these in its own <style>, which loads after this file and would otherwise
     win on equal specificity. The same trick the 16px zoom floor needs. */
  :root, html body{ overflow-x: clip; }
}

/* The bar owns ALL the space above it, and `.app` owns none.
 *
 * Split between the two, it was wrong twice over, and both were visible on a
 * phone (measured 7 Sept, three devices):
 *
 *   1. `.app`'s 18px top padding pushed the sticky bar down to y=18, so the
 *      painted header started 18px below the top of the screen and the status
 *      bar sat over a band of PAGE background. That is the gap.
 *   2. Pinned, the bar goes to y=0 and its padding is only the inset — so the
 *      logo moved 18px up the moment you scrolled and then sat flush against
 *      the notch with no breathing room.
 *
 * With the padding all on the bar, at rest and pinned are the same geometry:
 * background from y=0, content at inset + 28 (18 on a phone), and nothing
 * moves on scroll. `html body .app` because every page re-declares `.app` in
 * its own <style>, which loads after this file. */
html body .app{ padding-top: 0; }

.topbar{
  position: sticky;
  top: 0;
  /* Below the nav pill (40), the modal (50), the round menu (60) and the
     alert drawer (90/91). A header over an open dialog is a bug. */
  z-index: 30;
  padding-top: calc(var(--safe-top) + var(--app-pad-top));
}
.topbar::before{
  content: '';
  position: absolute;
  inset: 0 -100vw;
  z-index: -1;
  background: var(--bg-default);
}

/* The only thing that changes on scroll. The bar and the cards share a
   background, so with nothing between them a locked header reads as a chunk of
   the page that has stopped moving rather than a layer above it. `.is-stuck`
   is set by nav.js the moment the page leaves the top. */
.topbar.is-stuck{ box-shadow: var(--shadow-sm); }

/* ── Touch targets ──────────────────────────────────────────────────────────
   Keyed on pointer:coarse rather than width, because the thing that makes a
   control hard to hit is a finger, not a narrow window — an iPad at 768px gets
   no mobile breakpoint but has exactly the same problem. A Playwright audit
   found the round picker at 16px and the crest remove button at 19px. */
@media (pointer: coarse){
  .round-btn{ min-height: 40px; padding: 9px 2px; }
  .theme-toggle{ width: 42px; height: 42px; }
  /* 34px, and deliberately not the 40px the rest of this block uses.
   *
   * The remover sits on a 52px crest tile. At 40px it covers 77% of the tile
   * and becomes the biggest target on a card whose primary action is "open the
   * picker" — so the floor that protects every other control would, here, make
   * the wrong thing the easiest to hit. 34px clears WCAG 2.5.8's 24px minimum
   * with room to spare; it is the 44px practical touch bar it does not meet,
   * and that is the trade.
   *
   * Found 2026-09-08. `settings.test.js`'s touch-floor check had been passing
   * on an EMPTY crest grid — the suite mocks session/alerts/matches but not
   * `/api/teams`, so that call 401'd, no tiles rendered, and there was nothing
   * to measure. The test now mocks the roster and asserts this number, so the
   * exception is pinned rather than merely undetected. */
  .crest .clear, .card .clear{ min-width: 34px; min-height: 34px; padding: 8px; }
  .idbox input, .idbox .idgo{ min-height: 40px; }
  .btn, .btn.sm{ min-height: 40px; }
  /* Typed inputs, not a bare `input`: checkboxes and radios size themselves and
     a min-height would stretch their hit box oddly. `date` was the gap — the
     Activity range pickers sat at 35px because only text/number were listed. */
  select,
  input[type="text"], input[type="number"], input[type="date"],
  input[type="time"], input[type="search"], input[type="email"],
  input[type="password"], input[type="tel"]{ min-height: 40px; }

  /* iOS Safari zooms the whole page in when a control it focuses has a
     font-size under 16px, and does not zoom back out afterwards — that is the
     "it zooms in and out when I tap something". 16px here is a FLOOR on the
     control's own text, not a redesign of the form.

     The other well-known fix, `maximum-scale=1` on the viewport meta, also
     takes pinch-zoom away from everybody permanently, so it is not on the
     table. Checkboxes, radios and ranges are excluded: they carry no text and
     a font-size only distorts them.

     Prefixed `html` for the same reason `html body` is used below: each page's
     inline <style> loads AFTER this file, so a bare `select{font-size:13px}`
     there beats a bare `select` here on equal specificity. #eventPick stayed at
     13px — and so kept zooming — until this rule outranked it. */
  html input:not([type="checkbox"]):not([type="radio"]):not([type="range"]),
  html select, html textarea{ font-size: 16px; }

  /* Overlay and secondary controls that a Playwright/audit pass found below the
     40px floor on tablets (which get no max-width breakpoint) and in the modal
     and alert drawer. The visible glyph stays small; only the hit area grows. */
  .icon-btn{ min-width: 40px; min-height: 40px; }
  .alert-ack{ min-width: 40px; min-height: 40px; }
  .drawer-clear{ min-height: 40px; }
  .round-option{ min-height: 40px; }
  .sub-tabs button{ min-height: 40px; }
  .ac-exp{ min-width: 40px; min-height: 40px; }
  .ac-rb{ min-height: 40px; }
  .zone{ min-height: 44px; }
}
