/* =====================================================================
   Ideals Precision Series Rewards, the whole design system.

   The layout is taken from the Ideals design team's own Advisory Suite
   prototype, which is the house pattern this pilot has to sit inside. The
   COLOUR is taken from the official brand hub (brand-hub.idealsvdr.com); see
   the token block below, which is the single source of truth for it.

     - A LIGHT page by default. White ground, white surfaces, a soft neutral
       grey behind illustrations and inside pills. It also has a DARK scheme,
       which is the same shapes re-valued by role: a very dark teal-tinted
       neutral ground, surfaces that lift rather than cast, and Emerald finally
       doing the accent job the brand hub drew it for. See "the dark scheme"
       under the tokens.
     - One deep green hero card at the top of the content, with a soft disc
       motif bleeding off its right edge and two translucent panels inside it.
       It is the one card that is dark in BOTH schemes.
     - Reward cards: grey illustration block, a floating white points pill
       with the coin glyph, bold title, grey description, white bordered
       Redeem button. The illustration block STAYS PALE in dark mode, because
       the drawings on it have their fills baked into the SVG. It is the light
       island, and the --art-* tokens are how it holds.
     - Tabs under the hero: the active one is a light green pill, the rest are
       plain grey text, with a hairline under the whole row.
     - Balance history as a table with a grey icon tile per row, a tinted
       change pill and a grey remaining balance pill.
     - An appearance control in the profile menu: Auto, Light, Dark. Which one
       is live is decided in CSS off the data-theme attribute, because the
       markup does not know.

   Rules this file lives by:
     - Mobile first. Attendees arrive by scanning a QR code at the venue, so
       the phone is the primary surface. Everything works at 360px, nothing
       scrolls sideways, and the tab row is reachable with one thumb.
     - Tap targets are at least 44px on touch pointers.
     - Form controls are 16px so iOS does not zoom the viewport on focus.
     - TWO schemes, one set of roles. :root below is the light palette and is
       the source of truth for what every token MEANS. The dark scheme
       re-values those same tokens by role further down; no component rule
       knows which scheme it is in. <body> paints its own background and
       colour either way, so nothing bleeds through from the browser.
     - The palette is a fixed token set. Depth comes from borders, soft
       shadow and the green ramp, never from inventing a new hue.
     - No inline styles anywhere in the app (the CSP forbids them), so every
       one-off spacing has a class here.
     - Every interactive element states all of its moods: hover, active,
       focus-visible, disabled and busy. The focus ring is never removed.

   TYPOGRAPHY (feedback item A4). The brand hub specifies Ivy Presto Headline
   for headings of 32px and above, paired with Montserrat below that. It also
   permits a second pairing, Montserrat Semibold headings over Montserrat body,
   and that is the one this portal uses throughout. Ivy Presto is a licensed
   face and we hold no web licence for it, so it is deliberately absent rather
   than missing. Nothing here needs changing unless a licence is bought.
   ===================================================================== */

/* ------------------------------------------------------------ tokens */

:root {
  /* ==== THE OFFICIAL IDEALS PALETTE (brand-hub.idealsvdr.com) ==============

     These five greens and the neutral ramp are the brand's own values. They
     replaced a set this project had matched by eye off a screen recording, so
     nothing below is a guess any more and nothing below may be nudged.

     Two of them cannot be used interchangeably, and this is the whole reason
     the role tokens underneath exist:

       Emerald #70EB94 is BRIGHT. White text on it is 1.50:1, which fails
       every threshold there is. It is a colour for marks, dots, coin glyphs
       and small accents ON a dark green, never a ground for white type.

       Jade #12695C is the mid green that DOES carry white text, at 6.56:1.
       Anything white sits on Jade or darker, never on Emerald.

     Ratios quoted throughout are WCAG 2.1 contrast, computed not estimated.
     Nothing here ships below 4.5:1 for body text or 3:1 for large text. */
  --brand-deep: #022E34;      /* Deep Green, the primary. White on it: 14.55:1 */
  --brand-dark: #002325;      /* Dark Green, the deepest shade. White on it: 16.57:1 */
  --brand-jade: #12695C;      /* Jade Green. White on it: 6.56:1. THE white-text green */
  --brand-emerald: #70EB94;   /* Emerald Green, the accent. White on it: 1.50:1 — NEVER
                                 white text. Deep Green on it: 9.70:1, which is the
                                 permitted way to put words on Emerald. */
  --brand-light: #DDFFDA;     /* Light Green. Jade on it: 6.06:1, ink on it: 16.81:1 */

  /* The brand neutrals, verbatim: #FFFFFF #6D6D6D #9E9E9E #B1B1B1 #C9C9C9
     #E2E2E2. Two lighter steps (#F2F2F2, #F0F0F0, #FAFAFA) are extended off
     the same grey axis because the ramp stops at #E2E2E2 and the page needs a
     tint quieter than that for panels and zebra stripes. They are pure greys,
     so they stay on the brand's axis rather than reintroducing a hue. */

  /* ---- which native scheme this document is.
     Declared here rather than on the `html` reset below so it lives with the
     palette it belongs to and so the two dark blocks, which are `:root`
     selectors, can flip it. It is what makes form fields, the caret, the
     scrollbars and any UA widget follow the page instead of staying white. */
  color-scheme: light;

  /* ---- ground and surfaces */
  --bg: #FFFFFF;              /* the page behind everything */
  --surface: #FFFFFF;         /* cards, panels, tables, the content plate */
  --surface-2: #F2F2F2;       /* quiet fill: inputs, table headers, hovers */
  --lav: #F0F0F0;             /* the soft grey: illustrations, tiles, pills. Named --lav
                                 from when it was a lavender; the hue is gone, the name
                                 stays because three stylesheets reference it. */
  --lav-2: #E2E2E2;           /* its border and pressed state — brand neutral */
  --surface-3: var(--lav);    /* kept as an alias so older markup still lands right */
  --surface-alt: #FAFAFA;     /* zebra stripe */

  /* ---- the green ramp, mapped onto the roles this app actually has.
     The split is the safety mechanism: --accent may be a fill, a border or a
     mark; only --accent-d and --accent-dd are ever allowed under white text. */
  --hero-a: var(--brand-deep); /* hero gradient start */
  --hero-b: var(--brand-dark); /* hero gradient end */
  --hero-edge: transparent;    /* an inset hairline round the hero card. Nothing in
                                  light: the white plate under it is separation enough. */
  --hero-btn-line: var(--line-2);  /* the edge of a plain .btn sitting ON the hero. In
                                      light this IS --line-2 and nothing changes; the
                                      token exists because in dark the button's own fill
                                      is darker than the card it sits on, so the edge is
                                      the only thing delimiting the control. */
  --accent: var(--brand-jade); /* fills, borders, focus edges, marks. 6.56:1 on white,
                                  so it is visible as a hairline rather than a rumour. */
  --accent-d: var(--brand-deep);    /* filled buttons: WHITE ON THIS IS 14.55:1 */
  --accent-dd: var(--brand-dark);   /* its hover: white on this is 16.57:1 */
  --on-accent: #FFFFFF;             /* the ink that sits ON --accent-d and --accent-dd.
                                       A token because it INVERTS between schemes: light
                                       fills with Deep Green and writes white on it,
                                       dark fills with Emerald and writes Deep Green on
                                       it. A button may not hard-code either one. */
  --accent-ink: var(--brand-jade);  /* green TEXT on a light ground. 6.56:1 on white,
                                       6.06:1 on --accent-l, 5.76:1 on --lav */
  --accent-bright: var(--brand-emerald); /* Emerald, for marks and dots ON dark green
                                            only. Never a ground for white text. */
  --accent-l: var(--brand-light);   /* the light green pill and active tab */
  --accent-l2: #C6F2C2;             /* its border and pressed state, one step down from
                                       Light Green. Jade on it: 5.27:1 */
  --coin-a: var(--brand-emerald);   /* coin glyph, front disc */
  --coin-b: var(--brand-jade);      /* coin glyph, rim and the disc behind */

  /* ---- text */
  --ink: #10161C;             /* primary, 18.2:1 on white */
  --muted: #5D5D5D;           /* secondary, 6.58:1 on white, 6.10:1 on --surface-2 */
  --faint: #6D6D6D;           /* tertiary — brand neutral. 5.17:1 on white,
                                 4.62:1 on --surface-2, 4.54:1 on --lav */
  --hero-ink: #FFFFFF;
  --hero-ink-2: rgba(255, 255, 255, .9);  /* 4.71:1 at the worst point on the hero,
                                             which is where a panel overlaps the Jade disc */

  /* ---- lines. Brand neutrals. */
  --line: #E2E2E2;            /* the everyday hairline */
  --line-2: #C9C9C9;          /* the border a control takes */
  --line-3: #B1B1B1;          /* the border a control takes on hover */

  /* ---- status. Every tint has an ink that passes AA on it. Success is the
     only one drawn from the brand palette: the other three are traffic-light
     colours, which the hub does not legislate and which must stay learnable. */
  /* The one colour here with NO brand-hub equivalent, and it is flagged rather
     than quietly kept: the book has greens, yellows and purples, and no red at
     all. A refusal an attendee cannot distinguish from a confirmation is worse
     than an off-book hue, so red stays until design rules on it — feedback item
     H3, open. If they choose a book yellow instead, this token and --ring-danger
     are the only two places to change. */
  --danger: #B42318;
  --warn: #8A5A00;
  --ok: var(--accent-ink);
  --ok-ink: var(--accent-ink);  /* 6.30:1 on --ok-bg */
  --info-ink: #0B5CAB;
  --info-bg: #E8F1FB;
  --info-line: #CBE0F6;
  --warn-bg: #FBF2DF;
  --warn-line: #F0DFB6;
  --ok-bg: #EEFFEC;           /* Light Green, lifted one step so the pill inside it
                                 (which is Light Green itself) still separates */
  --ok-line: var(--accent-l2);
  --bad-bg: #FDECEA;
  --bad-line: #F6CFCA;
  --bad-bg-2: #FBDEDA;        /* the pressed/hover step of the red tint (.btn-danger) */

  /* The DEEP inks. A flash and a callout carry more words than a tag does, and
     they carry them on the same tint, so they take a darker ink than the status
     colour itself: --danger has to stay legible as a mark on white, these only
     ever appear on their own tint. They were three hard-coded hex values in the
     flash and callout rules until the dark scheme needed them to move. */
  --bad-ink: #8C1B12;         /* 8.05:1 on --bad-bg */
  --warn-ink: #6F4900;        /* 7.18:1 on --warn-bg */
  --info-deep-ink: #08487F;   /* 8.20:1 on --info-bg */
  --warn-edge: #C89A2B;       /* the 3px left edge of an amber flash or callout */

  /* Washes over whatever is beneath them. Light assumes a light ground and goes
     darker; dark inverts and goes lighter. Only these three, and only where a
     component genuinely cannot know its own ground. */
  --overlay-soft: rgba(16, 22, 28, .07);   /* .flash-dismiss:hover */
  --overlay-line: rgba(16, 22, 28, .14);   /* a border inside a tinted callout */
  --overlay-faint: rgba(16, 22, 28, .04);  /* its hover */
  --scrim: rgba(0, 35, 37, .5);            /* behind a modal. Dark Green, dimmed */

  /* ---- THE LIGHT ISLAND. These seven do not change between schemes.

     The reward and activity drawings in src/views/icons.js are inline SVG with
     their fills BAKED IN: #12695C, #022E34, #DDFFDA, #FFFFFF, #70EB94, #C9C9C9.
     CSS cannot recolour them, and they are drawn for a pale ground. Every Deep
     Green plane in every drawing would disappear into a dark plate.

     So .card-art stays LIGHT in both schemes, and everything that sits on it
     goes with it: the eyebrow, the floating points pill and the coin inside it.
     A light plate holding product artwork on a dark page is a normal, deliberate
     pattern; it reads as a photograph would. These are the values --lav, --faint,
     --surface, --ink, --accent-l and --accent-ink hold in LIGHT, forked into
     their own names so the originals are free to go dark for the tiles, badges,
     balance pills and status pills that should. */
  --art-plate: #F0F0F0;             /* the illustration block */
  --art-plate-2: #E2E2E2;           /* its hover */
  --art-ink: #6D6D6D;               /* the "Earn Points" eyebrow. 4.54:1 on the plate */
  --art-pill-bg: #FFFFFF;           /* the floating points pill */
  --art-pill-ink: #10161C;          /* 18.20:1 on it */
  --art-pill-green-bg: var(--brand-light);  /* the "Included" pill */
  --art-pill-green-ink: var(--brand-jade);  /* 6.06:1 on it */

  --font: 'Montserrat', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto,
    'Helvetica Neue', Arial, sans-serif;
  --mono: ui-monospace, 'SF Mono', Menlo, Consolas, monospace;

  /* ---- type scale. One ratio, used everywhere, so nothing is "roughly" a size. */
  --fs-2xs: .6875rem;   /* 11, eyebrows, tags */
  --fs-xs: .75rem;      /* 12, hints, footer, meta */
  --fs-sm: .8125rem;    /* 13, dense UI, secondary copy */
  --fs-md: .875rem;     /* 14, body copy in prose */
  --fs-base: .9375rem;  /* 15, default */
  --fs-lg: 1.0625rem;   /* 17, card and panel titles */
  --fs-xl: 1.375rem;    /* 22, page titles */
  --fs-2xl: 1.75rem;    /* 28, hero numbers, auth headings */
  --fs-3xl: 2.125rem;   /* 34, the one big number on the error page */

  --lh-tight: 1.2;
  --lh-heading: 1.28;
  --lh-body: 1.6;
  --track-caps: .1em;   /* letter-spacing for uppercase micro type */
  --track-tight: -.011em;

  /* ---- spacing rhythm. Everything vertical is a step on this scale. */
  --sp-1: .25rem;
  --sp-2: .5rem;
  --sp-3: .75rem;
  --sp-4: 1rem;
  --sp-5: 1.5rem;
  --sp-6: 2rem;
  --sp-7: 2.75rem;

  /* ---- form */
  --r-sm: 8px;
  --r-md: 12px;
  --r-lg: 16px;
  --r-pill: 999px;
  --tap: 44px;

  /* ---- depth. Soft and low, the way the reference reads. */
  --shadow-1: 0 1px 2px rgba(16, 24, 40, .05);
  --shadow-2: 0 1px 2px rgba(16, 24, 40, .06), 0 8px 20px -12px rgba(16, 24, 40, .22);
  --shadow-3: 0 24px 60px -24px rgba(16, 24, 40, .30), 0 2px 6px rgba(16, 24, 40, .06);
  --shadow-pill: 0 1px 3px rgba(16, 24, 40, .12), 0 4px 10px -6px rgba(16, 24, 40, .18);
  --ring: 0 0 0 4px rgba(18, 105, 92, .24);   /* Jade halo */
  --ring-danger: 0 0 0 4px rgba(180, 35, 24, .20);
  --hairline: inset 0 1px 0 rgba(255, 255, 255, .6);

  --ease: cubic-bezier(.2, .7, .3, 1);
  --dur: .16s;
}

/* ============================================================ the dark scheme

   THE SAME BLOCK, TWICE, AND IT MUST STAY THAT WAY.

     @media (prefers-color-scheme: dark) { :root:not([data-theme="light"]) }
        makes "follow my device" work. src/http/respond.js stamps NO attribute
        at all for that state, so this media query is the only thing deciding.

     :root[data-theme="dark"]
        makes an explicit choice beat the device, in both directions. The
        :not([data-theme="light"]) above is the other half of that: a page the
        attendee set to light stays light on a dark phone.

   Nothing below re-values a token the light block has not already defined. Every
   colour in this product has a light value on bare :root first; these two blocks
   only ever change what an existing role is worth. If you add a token, add it to
   :root, then to BOTH of these, with identical text.

   scripts/verify.js reads every stylesheet for "--name:" and for "var(--name)"
   and will not let a token be consumed that nothing defines. It cannot tell you
   that these two blocks have drifted apart, so: diff them by eye. */

@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) {
  color-scheme: dark;

  /* ---- ground and surfaces. Not black. A very dark, slightly teal-tinted
     neutral off the Deep Green hue sits with the brand and is easier to read
     against for the length of an event than #000 is. Surfaces LIFT rather than
     drop shadows onto the ground, because a shadow does almost nothing here:
     every step up the ladder is a lighter step, and the hairlines below carry
     the rest of the separation. */
  --bg: #060E10;
  --surface: #0B1719;
  --surface-2: #17272A;
  --lav: #1C2E30;
  --lav-2: #263B3E;
  --surface-alt: #101F21;

  /* ---- the green ramp, INVERTED BY ROLE.
     Emerald is 1.50:1 on white, so light can never put text in it. On this
     ground it is 12.17:1, and it finally becomes what the brand hub drew it to
     be: the accent. Jade steps back to being the rim of the coin. */
  --hero-a: #0A4C48;                /* Deep Green lifted half way to Jade, so the
                                       hero still reads as a raised card: 1.86:1 on
                                       --surface at this end of the gradient and
                                       1.25:1 at the other. White on it: 9.81:1. */
  --hero-b: var(--brand-deep);
  --hero-edge: rgba(112, 235, 148, .16);   /* closes the darker corner */
  --hero-btn-line: rgba(255, 255, 255, .55);  /* 4.15:1 on the lightest end of the hero
                                                 gradient, 5.38:1 on the darkest. A plain
                                                 button's fill is --surface, which is
                                                 DARKER than the hero here, so the edge is
                                                 what says "control" and it has to clear
                                                 the 3:1 floor on its own. */
  --accent: var(--brand-emerald);   /* 12.17:1 on --surface, so a hairline, a focus
                                       edge or a mark is unmistakable */
  --accent-d: var(--brand-emerald); /* filled buttons: the ink on them flips too */
  --accent-dd: var(--brand-light);  /* their hover, exactly as .hero .btn-primary
                                       already does it in light */
  --on-accent: var(--brand-deep);   /* 9.70:1 on Emerald, 13.44:1 on Light Green */
  --accent-ink: var(--brand-emerald);
  --accent-l: #123B36;              /* Jade brought down onto the ground: the active
                                       tab pill, the avatar, every green hover fill */
  --accent-l2: #1A5148;
  /* --accent-bright, --coin-a and --coin-b are deliberately NOT re-valued. The
     coin is one fixed object in both schemes: views/icons.js draws it with baked
     in fills and .dot has to stay the same object as that SVG. */

  /* ---- text */
  --ink: #E9EFEF;             /* 15.69:1 on --surface */
  --muted: #AAB7B9;           /* 8.85:1 on --surface, 6.87:1 on --lav */
  --faint: #98A6A8;           /* 7.26:1 on --surface, 6.14:1 on --surface-2 */
  /* --hero-ink and --hero-ink-2 are unchanged: the hero is dark in both. */

  /* ---- lines. Heavier than in light, because no shadow is doing the other
     half of the work here. --line-2 clears 3:1 on both grounds it is drawn on,
     which is the floor for a border that defines a control. */
  --line: #2A3A3D;
  --line-2: #5F7376;
  --line-3: #7F9396;

  /* ---- status. The same four meanings with the pair turned over: the ink is
     now the light half and the tint the dark half. Red is still off book (the
     hub has no red at all, feedback item H3) and a refusal still has to be told
     from a confirmation at a glance, so it stays. */
  --danger: #FF9E92;
  --warn: #F0C36C;
  --info-ink: #8CC2FF;
  --info-bg: #142737;
  --info-line: #2B5070;
  --warn-bg: #302614;
  --warn-line: #5E4A22;
  --ok-bg: #12332C;
  --bad-bg: #371815;
  --bad-line: #66302A;
  --bad-bg-2: #45201B;
  --bad-ink: #FFB8AE;
  --warn-ink: #F6D18E;
  --info-deep-ink: #ABD3FF;
  --warn-edge: #D9A93F;
  /* --ok, --ok-ink and --ok-line are var() aliases of the green ramp and follow
     it without being restated. */

  --overlay-soft: rgba(255, 255, 255, .10);
  --overlay-line: rgba(255, 255, 255, .22);
  --overlay-faint: rgba(255, 255, 255, .06);
  --scrim: rgba(0, 12, 13, .74);    /* deeper: a 50% wash over a dark page is no wash */

  /* ---- depth. A drop shadow is nearly invisible on a dark ground, so the
     lifting is done by the ladder and the hairlines above and these are pulled
     back to the one thing that still reads: a tight black edge under something
     raised. --shadow-1 may NOT become `none`, because admin-queue.css composes it
     a multi-value box-shadow, and `none` inside a list voids the declaration. */
  --shadow-1: 0 1px 2px rgba(0, 0, 0, .45);
  --shadow-2: 0 1px 2px rgba(0, 0, 0, .55), 0 10px 26px -14px rgba(0, 0, 0, .9);
  --shadow-3: 0 28px 66px -26px rgba(0, 0, 0, .95), 0 2px 8px rgba(0, 0, 0, .6);
  --ring: 0 0 0 4px rgba(112, 235, 148, .30);
  --ring-danger: 0 0 0 4px rgba(255, 158, 146, .26);
  --hairline: inset 0 1px 0 rgba(255, 255, 255, .06);
  /* --shadow-pill is NOT re-valued. Its only user is .pill, which lives on the
     light island and keeps its light shadow with it. Nor are the seven --art-*
     tokens: that is the whole point of them. */
  }
}

/* The explicit choice. Identical to the block above, on purpose. */
:root[data-theme="dark"] {
  color-scheme: dark;

  /* ---- ground and surfaces. Not black. A very dark, slightly teal-tinted
     neutral off the Deep Green hue sits with the brand and is easier to read
     against for the length of an event than #000 is. Surfaces LIFT rather than
     drop shadows onto the ground, because a shadow does almost nothing here:
     every step up the ladder is a lighter step, and the hairlines below carry
     the rest of the separation. */
  --bg: #060E10;
  --surface: #0B1719;
  --surface-2: #17272A;
  --lav: #1C2E30;
  --lav-2: #263B3E;
  --surface-alt: #101F21;

  /* ---- the green ramp, INVERTED BY ROLE.
     Emerald is 1.50:1 on white, so light can never put text in it. On this
     ground it is 12.17:1, and it finally becomes what the brand hub drew it to
     be: the accent. Jade steps back to being the rim of the coin. */
  --hero-a: #0A4C48;                /* Deep Green lifted half way to Jade, so the
                                       hero still reads as a raised card: 1.86:1 on
                                       --surface at this end of the gradient and
                                       1.25:1 at the other. White on it: 9.81:1. */
  --hero-b: var(--brand-deep);
  --hero-edge: rgba(112, 235, 148, .16);   /* closes the darker corner */
  --hero-btn-line: rgba(255, 255, 255, .55);  /* 4.15:1 on the lightest end of the hero
                                                 gradient, 5.38:1 on the darkest. A plain
                                                 button's fill is --surface, which is
                                                 DARKER than the hero here, so the edge is
                                                 what says "control" and it has to clear
                                                 the 3:1 floor on its own. */
  --accent: var(--brand-emerald);   /* 12.17:1 on --surface, so a hairline, a focus
                                       edge or a mark is unmistakable */
  --accent-d: var(--brand-emerald); /* filled buttons: the ink on them flips too */
  --accent-dd: var(--brand-light);  /* their hover, exactly as .hero .btn-primary
                                       already does it in light */
  --on-accent: var(--brand-deep);   /* 9.70:1 on Emerald, 13.44:1 on Light Green */
  --accent-ink: var(--brand-emerald);
  --accent-l: #123B36;              /* Jade brought down onto the ground: the active
                                       tab pill, the avatar, every green hover fill */
  --accent-l2: #1A5148;
  /* --accent-bright, --coin-a and --coin-b are deliberately NOT re-valued. The
     coin is one fixed object in both schemes: views/icons.js draws it with baked
     in fills and .dot has to stay the same object as that SVG. */

  /* ---- text */
  --ink: #E9EFEF;             /* 15.69:1 on --surface */
  --muted: #AAB7B9;           /* 8.85:1 on --surface, 6.87:1 on --lav */
  --faint: #98A6A8;           /* 7.26:1 on --surface, 6.14:1 on --surface-2 */
  /* --hero-ink and --hero-ink-2 are unchanged: the hero is dark in both. */

  /* ---- lines. Heavier than in light, because no shadow is doing the other
     half of the work here. --line-2 clears 3:1 on both grounds it is drawn on,
     which is the floor for a border that defines a control. */
  --line: #2A3A3D;
  --line-2: #5F7376;
  --line-3: #7F9396;

  /* ---- status. The same four meanings with the pair turned over: the ink is
     now the light half and the tint the dark half. Red is still off book (the
     hub has no red at all, feedback item H3) and a refusal still has to be told
     from a confirmation at a glance, so it stays. */
  --danger: #FF9E92;
  --warn: #F0C36C;
  --info-ink: #8CC2FF;
  --info-bg: #142737;
  --info-line: #2B5070;
  --warn-bg: #302614;
  --warn-line: #5E4A22;
  --ok-bg: #12332C;
  --bad-bg: #371815;
  --bad-line: #66302A;
  --bad-bg-2: #45201B;
  --bad-ink: #FFB8AE;
  --warn-ink: #F6D18E;
  --info-deep-ink: #ABD3FF;
  --warn-edge: #D9A93F;
  /* --ok, --ok-ink and --ok-line are var() aliases of the green ramp and follow
     it without being restated. */

  --overlay-soft: rgba(255, 255, 255, .10);
  --overlay-line: rgba(255, 255, 255, .22);
  --overlay-faint: rgba(255, 255, 255, .06);
  --scrim: rgba(0, 12, 13, .74);    /* deeper: a 50% wash over a dark page is no wash */

  /* ---- depth. A drop shadow is nearly invisible on a dark ground, so the
     lifting is done by the ladder and the hairlines above and these are pulled
     back to the one thing that still reads: a tight black edge under something
     raised. --shadow-1 may NOT become `none`, because admin-queue.css composes it
     a multi-value box-shadow, and `none` inside a list voids the declaration. */
  --shadow-1: 0 1px 2px rgba(0, 0, 0, .45);
  --shadow-2: 0 1px 2px rgba(0, 0, 0, .55), 0 10px 26px -14px rgba(0, 0, 0, .9);
  --shadow-3: 0 28px 66px -26px rgba(0, 0, 0, .95), 0 2px 8px rgba(0, 0, 0, .6);
  --ring: 0 0 0 4px rgba(112, 235, 148, .30);
  --ring-danger: 0 0 0 4px rgba(255, 158, 146, .26);
  --hairline: inset 0 1px 0 rgba(255, 255, 255, .06);
  /* --shadow-pill is NOT re-valued. Its only user is .pill, which lives on the
     light island and keeps its light shadow with it. Nor are the seven --art-*
     tokens: that is the whole point of them. */
}

/* Montserrat's lowercase runs slightly wide at large sizes; when we know it is
   really loaded (layout.js stamps data-fonts) we can tighten headings a touch.
   With the system stack the tracking stays at 0, which is what those faces want. */
html[data-fonts="hosted"] h1,
html[data-fonts="hosted"] h2,
html[data-fonts="hosted"] .brand-word,
html[data-fonts="hosted"] .hcard-points,
html[data-fonts="hosted"] .stat .v { letter-spacing: var(--track-tight); }

/* ------------------------------------------------------------ reset */

* { box-sizing: border-box; }

html {
  /* color-scheme is NOT set here. It lives on :root with the palette, where the
     two dark blocks can flip it; a rule on `html` would be a second source of
     truth that could never win the cascade against them anyway. */
  -webkit-text-size-adjust: 100%;
  background: var(--bg);
}

body {
  margin: 0;
  padding: 0;
  background: var(--bg);
  color: var(--ink);
  font-family: var(--font);
  font-size: var(--fs-base);
  font-weight: 400;
  line-height: var(--lh-body);
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  overflow-x: hidden;
  min-height: 100vh;
  text-rendering: optimizeLegibility;
}

h1, h2, h3, h4 {
  margin: 0;
  font-weight: 700;
  line-height: var(--lh-heading);
  letter-spacing: var(--track-tight);
  color: var(--ink);
}
h3, h4 { font-weight: 600; letter-spacing: 0; }
p { margin: 0; }
a { color: inherit; }
img { max-width: 100%; height: auto; }
svg { display: block; }
hr { border: 0; border-top: 1px solid var(--line); margin: var(--sp-5) 0; }
b, strong { font-weight: 700; }
small { font-size: var(--fs-xs); }

/* [hidden] must win over any display rule below it: the modal roots and the
   client-side error hints rely on it. */
[hidden] { display: none !important; }

/* One ring, everywhere, and it survives on every background: a solid green
   outline plus a soft halo, so it reads over the white plate and over the deep
   green hero alike. Never trade this for a "cleaner" look. */
:focus-visible {
  outline: 2px solid var(--accent-d);
  outline-offset: 2px;
  border-radius: 4px;
  box-shadow: var(--ring);
}

/* Inside the hero the ground is dark, so the ring flips to white with an
   Emerald halo rather than disappearing into the gradient. This is exactly
   what Emerald is for: a mark on a deep green ground, carrying no text. */
.hero :focus-visible {
  outline-color: #FFFFFF;
  box-shadow: 0 0 0 4px rgba(112, 235, 148, .45);
}

/* ------------------------------------------------------------ accessibility */

.visually-hidden {
  position: absolute !important;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0 0 0 0);
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

.skip-link {
  position: absolute;
  left: var(--sp-2);
  top: -4rem;
  z-index: 100;
  background: var(--accent-d);
  color: var(--on-accent);
  padding: var(--sp-3) var(--sp-4);
  border-radius: 0 0 var(--r-sm) var(--r-sm);
  font-size: var(--fs-sm);
  font-weight: 600;
  text-decoration: none;
  box-shadow: var(--shadow-2);
  transition: top var(--dur) var(--ease);
}
.skip-link:focus { top: 0; }

/* The main region gets tabindex="-1" so the skip link can move focus to it. */
[tabindex="-1"]:focus { outline: none; box-shadow: none; }

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

.phone {
  background: var(--surface);
  max-width: 64rem;
  margin: 0 auto;
  min-height: 100vh;
  display: flex;
  flex-direction: column;
}

@supports (min-height: 100dvh) {
  .phone { min-height: 100dvh; }
}

/* The sign-in screens, attendee and concierge alike, carry no navigation, no
   hero and no tab row, and .authwrap already holds the form to 24rem. Left at
   the portal's 64rem the shell is a very wide white card with a narrow column
   floating in the middle of it. Closing the shell around the column is what
   makes it read as a sign-in screen rather than an empty page. Below 34rem the
   max-width does nothing, so a phone is unaffected. */
.page-auth .phone { max-width: 34rem; }

@media (min-width: 48rem) {
  .phone {
    border: 1px solid var(--line);
    border-radius: var(--r-lg);
    overflow: hidden;
    min-height: 0;
    margin: var(--sp-5) auto var(--sp-7);
    box-shadow: var(--shadow-2);
  }
}

/* The attendee holds a phone. The concierge sits at a desk with an audit log
   open, and 64rem is not a desk: it leaves an audit row 976px to fit six
   columns into, two of which are a corporate address and an IPv6 address, and
   the table spends the whole event scrolling sideways for want of 150px that
   the screen already had. The admin shell alone widens once the viewport can
   actually hold it, which keeps 48px of page either side at 1280. Below that
   this rule does nothing and both surfaces share the 64rem shell. */
@media (min-width: 78rem) {
  .page-admin .phone { max-width: 74rem; }
}

.topbar {
  display: flex;
  align-items: center;
  gap: var(--sp-3);
  padding: var(--sp-3) var(--sp-4);
  border-bottom: 1px solid var(--line);
  background: var(--surface);
  position: relative;
}

/* ---- brand lockup: mark, wordmark, programme eyebrow */

.brand {
  display: inline-flex;
  align-items: center;
  gap: .625rem;
  min-width: 0;
  text-decoration: none;
  color: inherit;
  border-radius: var(--r-sm);
  padding: 2px;
  margin: -2px;
  transition: opacity var(--dur) var(--ease);
}
a.brand:hover { opacity: .78; }
a.brand:active { opacity: .64; }

/* The symbol is never rendered beside the wordmark — the hub forbids the pair
   and views/icons.js explains it — so this styles nothing today. Kept with the
   mark it belongs to, for the symbol-only contexts. */
.brand-mark {
  flex: 0 0 auto;
  border-radius: 9px;
  box-shadow: 0 2px 6px -2px rgba(2, 46, 52, .45);
}

.brand-lines {
  display: flex;
  flex-direction: column;
  min-width: 0;
  line-height: 1.1;
}
.brand-word {
  font-size: 1.0625rem;
  font-weight: 700;
  color: var(--ink);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.brand-eyebrow {
  font-size: .625rem;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: .13em;
  color: var(--accent-ink);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  margin-top: 2px;
}

/* The lockup already names the programme, so the breadcrumb is desktop only.
   On a 360px screen those pixels belong to the content. */
.crumbs {
  display: none;
  align-items: center;
  gap: var(--sp-2);
  font-size: var(--fs-sm);
  min-width: 0;
  padding-left: var(--sp-3);
  margin-left: var(--sp-1);
  border-left: 1px solid var(--line);
}
.crumbs a { text-decoration: none; }
.crumb-root { color: var(--muted); }
.crumb-sep { color: var(--faint); }
.crumb-leaf {
  font-weight: 600;
  color: var(--ink);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.tb-act {
  display: flex;
  align-items: center;
  gap: var(--sp-2);
  flex-shrink: 0;
  margin-left: auto;
}
/* Only a form sitting DIRECTLY in the action row, which is a single control
   dressed as a button: sign out, the help toggle. The profile menu hangs off
   this row too and its panel holds two forms of its own, one of them the
   appearance control, which is a labelled block and not a button. Without the
   child combinator this rule reached inside the panel and out-specified both
   of them, and the segmented control came out inline and clipped by the panel
   edge. */
.tb-act > form { margin: 0; display: inline-flex; }

.icon-btn,
.avatar {
  width: 36px;
  height: 36px;
  border-radius: 50%;
  display: grid;
  place-items: center;
  font-size: var(--fs-xs);
  text-decoration: none;
  flex-shrink: 0;
}

.icon-btn {
  color: var(--muted);
  border: 1px solid var(--line-2);
  background: var(--surface);
  font-family: inherit;
  font-weight: 600;
  cursor: pointer;
  position: relative;
  padding: 0;
  transition: color var(--dur) var(--ease), border-color var(--dur) var(--ease),
    background-color var(--dur) var(--ease);
}
.icon-btn:hover { color: var(--accent-ink); border-color: var(--accent); background: var(--accent-l); }
.icon-btn:active { background: var(--accent-l2); }

/* Keeps the 36px look but gives the control a 44px hit area on touch. */
.icon-btn::after {
  content: '';
  position: absolute;
  inset: -4px;
  border-radius: 50%;
}

.avatar {
  background: var(--accent-l);
  border: 1px solid var(--accent-l2);
  font-size: var(--fs-2xs);
  font-weight: 700;
  color: var(--accent-ink);
  letter-spacing: .02em;
}

.wrap {
  padding: var(--sp-4) var(--sp-4) var(--sp-6);
  flex: 1 1 auto;
  min-width: 0;
  background: var(--surface);
}

/* ------------------------------------------------------------ hero */

.hero {
  position: relative;
  overflow: hidden;
  border-radius: var(--r-lg);
  padding: var(--sp-5) var(--sp-4);
  background: linear-gradient(115deg, var(--hero-a) 0%, var(--hero-b) 100%);
  color: var(--hero-ink);
  margin-bottom: var(--sp-4);
  isolation: isolate;
  /* An inset hairline rather than a border, so the card's geometry is identical
     in both schemes. Transparent in light, where a deep green card on a white
     plate needs no help. In dark the page is a dark teal neutral and the darker
     corner of the gradient closes to 1.25:1 against it, which is a soft edge for
     the one card the whole screen is built around; the Emerald line finishes it. */
  box-shadow: inset 0 0 0 1px var(--hero-edge);
}

/* The disc motif: two soft discs, the larger one bleeding off the right edge
   and a lighter ellipse in front of it. Drawn by the card itself, in CSS, so
   there is no image to load, nothing for the CSP to allow, and no extra
   element for a view to remember. Both sit at z-index -1, which inside the
   card's own stacking context puts them above its gradient and below every
   word on it.

   THE CEILING ON THESE COLOURS IS JADE. Hero text is white and the panels are
   only 9% white over whatever is behind them, so a disc is a text background
   whether it was meant as one or not. Jade #12695C is the lightest value
   allowed here: white on it is 6.56:1, and 5.40:1 once the panel's own 9%
   white wash is composited on top. Emerald would take that to 1.50:1, which is
   why the accent green appears on this card as the coin and the focus halo and
   nowhere else. */
.hero::before,
.hero::after {
  content: '';
  position: absolute;
  z-index: -1;
  border-radius: 50%;
  top: 50%;
  pointer-events: none;
}
.hero::before {
  top: 44%;
  right: -6.5rem;
  width: 21rem;
  height: 21rem;
  margin-top: -10.5rem;
  background: radial-gradient(circle at 42% 24%,
    #0A4245 0%,
    #08393E 48%,
    #06323A 84%,
    #053039 97%,
    rgba(5, 48, 57, 0) 100%);
  opacity: .95;
}
.hero::after {
  top: 56%;
  right: 1rem;
  width: 11rem;
  height: 11rem;
  margin-top: -5.5rem;
  background: radial-gradient(circle at 32% 22%,
    #12695C 0%,
    #105F55 46%,
    #0D544E 84%,
    #0C4F4B 97%,
    rgba(12, 79, 75, 0) 100%);
  opacity: .95;
  box-shadow: -30px 8px 56px -32px rgba(0, 0, 0, .6);
}

/* The old markup carried an empty <div class="hero-deco"> for the motif. The
   card draws it now, so the element is kept inert rather than removed: a view
   that still emits it renders correctly instead of stacking a second pair of
   discs on the first. */
.hero-deco { display: none; }

.hero-hi {
  font-size: var(--fs-xl);
  font-weight: 700;
  line-height: var(--lh-heading);
  color: var(--hero-ink);
  margin-bottom: var(--sp-4);
  position: relative;
  overflow-wrap: anywhere;
}

.hero-cards {
  display: grid;
  gap: var(--sp-3);
  position: relative;
}

/* The two translucent panels. Lighter green over the gradient rather than a
   flat fill, so the disc behind them still shows through at the edges. */
.hcard {
  background: rgba(255, 255, 255, .09);
  border: 1px solid rgba(255, 255, 255, .13);
  border-radius: var(--r-md);
  padding: var(--sp-4);
  min-width: 0;
  backdrop-filter: blur(6px);
}
.hcard-t {
  display: flex;
  align-items: center;
  gap: .4rem;
  font-size: var(--fs-base);
  font-weight: 600;
  color: var(--hero-ink);
  margin-bottom: var(--sp-2);
}
.hcard-points {
  display: flex;
  align-items: center;
  gap: .45rem;
  font-size: var(--fs-2xl);
  font-weight: 700;
  line-height: 1.1;
  color: var(--hero-ink);
  font-variant-numeric: tabular-nums;
  margin-bottom: var(--sp-2);
}
.hcard-sub {
  font-size: var(--fs-sm);
  color: var(--hero-ink-2);
  line-height: 1.5;
}
.hcard-note {
  font-size: var(--fs-xs);
  color: #FCF2DC;   /* Not tokenised: the hero is a dark card in BOTH schemes, so this
                       cream reads on it either way. 4.85:1 at the worst point on the
                       light hero, 13.08:1 on Deep Green, 8.82:1 at the lightest point
                       of the dark hero. */
  margin-top: var(--sp-2);
  line-height: 1.5;
}
.hcard .btn { margin-top: var(--sp-3); }

/* The coin sits next to every points figure in the reference, so it has to
   read on the hero as well as on a white pill. .dot is the CSS drawn version
   for markup that has no room for an SVG; .coin is the inline SVG from
   views/icons.js. They are the same object. */
.dot {
  width: 16px;
  height: 16px;
  border-radius: 50%;
  flex-shrink: 0;
  background:
    linear-gradient(100deg, var(--coin-b) 0 34%, var(--coin-a) 34% 100%);
  box-shadow: inset 0 0 0 1px var(--coin-b);
}
.dot-warn { background: var(--warn); box-shadow: none; }
.dot-danger { background: var(--danger); box-shadow: none; }
.dot-faint { background: var(--faint); box-shadow: none; }

.coin { flex-shrink: 0; }

/* ------------------------------------------------------------ guide strip */

.guide {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--sp-4);
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--r-md);
  padding: var(--sp-3) var(--sp-4);
  font-size: var(--fs-sm);
  color: var(--muted);
  margin-bottom: var(--sp-5);
  text-decoration: none;
  min-height: var(--tap);
  box-shadow: var(--shadow-1);
  transition: border-color var(--dur) var(--ease), background-color var(--dur) var(--ease),
    transform var(--dur) var(--ease);
}
.guide > * { min-width: 0; }
.guide strong { color: var(--ink); }
.guide span:last-child { color: var(--accent-ink); font-weight: 700; }
.guide:hover { border-color: var(--accent); background: var(--accent-l); }
.guide:active { transform: translateY(1px); }

/* ------------------------------------------------------------ tabs */

.tabs {
  display: flex;
  gap: var(--sp-1);
  border-bottom: 1px solid var(--line);
  margin-bottom: var(--sp-5);
  overflow-x: auto;
  -webkit-overflow-scrolling: touch;
  scrollbar-width: none;
  padding-bottom: var(--sp-1);
}
.tabs::-webkit-scrollbar { display: none; }

.tab {
  padding: var(--sp-3) var(--sp-4);
  font-size: var(--fs-md);
  font-weight: 500;
  color: var(--muted);
  white-space: nowrap;
  background: none;
  border: 0;
  border-radius: var(--r-sm);
  font-family: inherit;
  cursor: pointer;
  text-decoration: none;
  min-height: 44px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  /* three tabs, one thumb: they share the width on a phone rather than
     huddling in the left corner. */
  flex: 1 1 auto;
  transition: color var(--dur) var(--ease), background-color var(--dur) var(--ease);
}
.tab:hover { color: var(--ink); background: var(--surface-2); }
.tab:active { background: var(--lav); }
.tab.on,
.tab[aria-current="page"] {
  color: var(--accent-ink);
  font-weight: 700;
  background: var(--accent-l);
}
.tab.on:hover,
.tab[aria-current="page"]:hover { background: var(--accent-l2); color: var(--accent-ink); }

/* ------------------------------------------------------------ admin nav */

.adminnav {
  display: flex;
  gap: var(--sp-1);
  border-bottom: 1px solid var(--line);
  margin-bottom: var(--sp-5);
  overflow-x: auto;
  -webkit-overflow-scrolling: touch;
  scrollbar-width: none;
  padding-bottom: var(--sp-1);
}
.adminnav::-webkit-scrollbar { display: none; }

.adminnav a {
  display: inline-flex;
  align-items: center;
  gap: .4rem;
  padding: var(--sp-2) var(--sp-3);
  min-height: 44px;
  font-size: var(--fs-sm);
  color: var(--muted);
  white-space: nowrap;
  text-decoration: none;
  border-radius: var(--r-sm);
  transition: color var(--dur) var(--ease), background-color var(--dur) var(--ease);
}
.adminnav a:hover { color: var(--ink); background: var(--surface-2); }
.adminnav a[aria-current="page"] {
  color: var(--accent-ink);
  font-weight: 700;
  background: var(--accent-l);
}

.badge {
  display: inline-block;
  min-width: 1.35rem;
  padding: .05rem .35rem;
  border-radius: var(--r-pill);
  background: var(--lav);
  color: var(--ink);
  font-size: var(--fs-2xs);
  font-weight: 700;
  text-align: center;
  font-variant-numeric: tabular-nums;
  line-height: 1.4;
}
.badge-warn { background: var(--warn-bg); color: var(--warn); }
.badge-info { background: var(--info-bg); color: var(--info-ink); }

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

.grid { display: grid; gap: var(--sp-5); }

.card {
  display: flex;
  flex-direction: column;
  gap: var(--sp-2);
  min-width: 0;
}

/* The soft grey illustration block. Generous padding, big radius, and the
   points pill floating over its top right corner. */
/* THE LIGHT ISLAND. This block stays pale in dark mode, and the tokens say so.
   The drawings inside it come from src/views/icons.js with their fills baked
   into the SVG, so CSS cannot recolour them; every Deep Green plane in every
   reward would disappear into a dark plate. A light plate holding product
   artwork on a dark page is a normal, deliberate pattern. The eyebrow and the
   floating points pill below go light with it, because they sit on it. */
.card-art {
  position: relative;
  background: var(--art-plate);
  border-radius: var(--r-md);
  min-height: 168px;
  display: grid;
  place-items: center;
  padding: var(--sp-6) var(--sp-5);
  overflow: hidden;
  margin-bottom: var(--sp-2);
  transition: background-color var(--dur) var(--ease);
}
.card:hover .card-art { background: var(--art-plate-2); }
.card-art .pill { position: absolute; top: var(--sp-3); right: var(--sp-3); }
.card-art svg { width: auto; height: auto; max-width: 100%; }
.card-label {
  position: absolute;
  top: var(--sp-3);
  left: var(--sp-4);
  font-size: var(--fs-2xs);
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: .07em;
  color: var(--art-ink);
}
.card-name {
  font-size: var(--fs-base);
  font-weight: 700;
  color: var(--ink);
  line-height: 1.35;
  overflow-wrap: anywhere;
}
.card-desc { font-size: var(--fs-sm); color: var(--muted); line-height: 1.55; }
.card form { margin-top: var(--sp-2); }

/* The floating points pill: white, softly shadowed, coin then figure. It also
   carries a word instead of a number when a reward is free. */
.pill {
  display: inline-flex;
  align-items: center;
  gap: .35rem;
  background: var(--art-pill-bg);
  border-radius: var(--r-pill);
  padding: .3rem .7rem .3rem .5rem;
  font-size: var(--fs-sm);
  font-weight: 700;
  color: var(--art-pill-ink);
  font-variant-numeric: tabular-nums;
  box-shadow: var(--shadow-pill);
  line-height: 1.5;
}
.pill-green { background: var(--art-pill-green-bg); color: var(--art-pill-green-ink); box-shadow: none; }
.pill-lav { background: var(--art-plate); color: var(--art-pill-ink); box-shadow: none; }
/* An item that comes with the ticket has no price. Rendering its notional
   worth in the same green coin pill as a 200 or 600 point reward read as a cost
   that never leaves the balance, so it says the word instead. */
.pill-included {
  background: var(--art-pill-green-bg);
  color: var(--art-pill-green-ink);
  box-shadow: none;
  text-transform: uppercase;
  letter-spacing: .06em;
  font-size: var(--fs-2xs);
}

/* The forms that keep Rewards and Earn Points working when /assets/portal.js
   never arrives. Ordinary markup, hidden by portal.js the moment it does. They
   are set apart so they do not read as a second copy of the cards above. */
.nojs-forms {
  margin-top: var(--sp-7);
  padding-top: var(--sp-5);
  border-top: 1px solid var(--line);
}
.nojs-forms[hidden] { display: none; }

/* A page-level note that is about the page, not about the card above it. The
   points-to-money line used to sit under the last reward as a bare hint, in
   the same class, size and colour as that card's own hint, so on a phone it
   read as a second line about a 600 point vest. */
.panel-note {
  background: var(--lav);
  border: 1px solid var(--line);
}

/* ------------------------------------------------------------ buttons */

/* The default button is the reference's Redeem control: white, light grey
   border, rounded, dark label. */
.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: .45rem;
  padding: var(--sp-3) var(--sp-5);
  border-radius: var(--r-sm);
  border: 1px solid var(--line-2);
  background: var(--surface);
  color: var(--ink);
  font-family: var(--font);
  font-size: var(--fs-sm);
  font-weight: 600;
  letter-spacing: .01em;
  cursor: pointer;
  min-height: var(--tap);
  text-decoration: none;
  text-align: center;
  box-shadow: var(--shadow-1);
  transition: background-color var(--dur) var(--ease), border-color var(--dur) var(--ease),
    color var(--dur) var(--ease), transform var(--dur) var(--ease), opacity var(--dur) var(--ease);
}
/* A plain button on the hero. This sits ABOVE :hover, :disabled and the
   .hero .btn-primary block ON PURPOSE: every one of those is later in the
   cascade at equal or higher specificity, so each still wins its own state and
   this only supplies the resting edge. In light --hero-btn-line resolves to
   --line-2, which is the border the button already had. */
.hero .btn { border-color: var(--hero-btn-line); }

.btn:hover { border-color: var(--accent); background: var(--accent-l); color: var(--accent-ink); }
.btn:active { transform: translateY(1px); box-shadow: none; }
.btn:disabled,
.btn[aria-disabled="true"] {
  opacity: .5;
  cursor: not-allowed;
  box-shadow: none;
  transform: none;
}
.btn:disabled:hover,
.btn[aria-disabled="true"]:hover {
  border-color: var(--line-2);
  background: var(--surface);
  color: var(--ink);
}

.btn-primary {
  background: var(--accent-d);
  border-color: var(--accent-d);
  color: var(--on-accent);
  font-weight: 700;
  box-shadow: var(--shadow-1);
}
.btn-primary:hover { background: var(--accent-dd); border-color: var(--accent-dd); color: var(--on-accent); }
.btn-primary:active { box-shadow: none; }
.btn-primary:disabled,
.btn-primary[aria-disabled="true"] { box-shadow: none; }
.btn-primary:disabled:hover,
.btn-primary[aria-disabled="true"]:hover {
  background: var(--accent-d);
  border-color: var(--accent-d);
  color: var(--on-accent);
}

/* Inside the hero the primary button cannot stay Deep Green: the card it sits
   on IS Deep Green, so the control would vanish into it. This is the one place
   Emerald belongs as a ground, because the label flips to Deep Green with it —
   Deep Green on Emerald is 9.70:1, and it is one of the two combinations the
   brand hub actually permits. Hover moves to Light Green, 13.44:1, so the
   state change is obvious without leaving the palette. */
.hero .btn-primary {
  background: var(--accent-bright);
  border-color: var(--accent-bright);
  color: var(--brand-deep);
}
.hero .btn-primary:hover {
  background: var(--brand-light);
  border-color: var(--brand-light);
  color: var(--brand-deep);
}
.hero .btn-primary:disabled,
.hero .btn-primary[aria-disabled="true"],
.hero .btn-primary:disabled:hover,
.hero .btn-primary[aria-disabled="true"]:hover {
  background: var(--accent-bright);
  border-color: var(--accent-bright);
  color: var(--brand-deep);
}
/* The global busy spinner is white, which would disappear on Emerald. */
.hero .btn-primary[aria-busy="true"]::before { color: var(--brand-deep); }

.btn-sm { padding: .4rem var(--sp-3); min-height: var(--tap); font-size: var(--fs-xs); }
.btn-pill { border-radius: var(--r-pill); }
.btn-danger {
  border-color: var(--bad-line);
  color: var(--danger);
  background: var(--bad-bg);
}
.btn-danger:hover { border-color: var(--danger); background: var(--bad-bg-2); color: var(--danger); }
.btn-danger:focus-visible { outline-color: var(--danger); box-shadow: var(--ring-danger); }
.btn-row { display: flex; flex-wrap: wrap; gap: var(--sp-2); align-items: center; }

/* Busy: portal.js and admin.js set disabled plus aria-busy on submit, and swap
   in data-loading-text. This is the visible half of that contract, a spinner in
   front of the label so the control obviously did something. */
.btn[aria-busy="true"] {
  opacity: 1;
  cursor: progress;
  color: transparent;
  position: relative;
  pointer-events: none;
}
.btn[aria-busy="true"]::before {
  content: '';
  position: absolute;
  width: 1.05rem;
  height: 1.05rem;
  border-radius: 50%;
  border: 2px solid currentColor;
  border-top-color: transparent;
  border-right-color: transparent;
  color: var(--muted);
  opacity: .95;
  animation: spin .62s linear infinite;
}
.btn-primary[aria-busy="true"]::before { color: var(--on-accent); }
.btn-danger[aria-busy="true"]::before { color: var(--danger); }

@keyframes spin { to { transform: rotate(360deg); } }

.linkbtn {
  background: none;
  border: 0;
  padding: 0;
  margin: 0;
  font: inherit;
  color: inherit;
  cursor: pointer;
  text-decoration: none;
  border-radius: var(--r-sm);
}
.linkbtn[aria-busy="true"] { opacity: .6; cursor: progress; }

.hint { font-size: var(--fs-xs); color: var(--faint); margin-top: var(--sp-2); line-height: 1.5; }
.hint-error { color: var(--danger); }

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

.tablewrap {
  overflow-x: auto;
  -webkit-overflow-scrolling: touch;
  border: 1px solid var(--line);
  border-radius: var(--r-md);
  background: var(--surface);
  box-shadow: var(--shadow-1);
}

table { width: 100%; border-collapse: collapse; font-size: var(--fs-sm); }
th {
  text-align: left;
  padding: var(--sp-3) var(--sp-4);
  background: var(--surface-2);
  font-size: var(--fs-sm);
  color: var(--ink);
  font-weight: 700;
  white-space: nowrap;
  border-bottom: 1px solid var(--line);
}
td {
  padding: var(--sp-3) var(--sp-4);
  border-top: 1px solid var(--line);
  vertical-align: middle;
  line-height: 1.5;
  color: var(--ink);
}
tbody tr:first-child td { border-top: 0; }
tbody tr:nth-child(even) td { background: var(--surface-alt); }
/* A figure is read by its shape, so it is right aligned, set in tabular figures
   so the columns of digits line up down the table, and never wrapped: "1,573"
   broken after the comma is two numbers. */
td.num, th.num {
  text-align: right;
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}
td.nowrap, th.nowrap { white-space: nowrap; }
tbody tr { transition: background-color var(--dur) var(--ease); }
tbody tr:hover td { background: var(--surface-2); }
caption {
  caption-side: top;
  text-align: left;
  padding: var(--sp-3) var(--sp-4);
  font-size: var(--fs-2xs);
  text-transform: uppercase;
  letter-spacing: .08em;
  color: var(--faint);
  font-weight: 700;
  border-bottom: 1px solid var(--line);
}

/* ---- the balance history row anatomy

   Activity | Balance change | Remaining balance | Date, where the activity
   cell carries a soft grey icon tile, a bold name and a grey subtitle, the
   change is a tinted pill with a sign, and the remaining balance is a
   grey pill. Every one of these is a plain class so a view can build the
   row without a single inline style. */

.cell-lead {
  display: flex;
  align-items: center;
  gap: var(--sp-3);
  min-width: 0;
}
.tile {
  flex: 0 0 auto;
  width: 40px;
  height: 40px;
  border-radius: 10px;
  background: var(--lav);
  color: var(--accent-ink);
  display: grid;
  place-items: center;
}
.tile svg { width: 20px; height: 20px; }
.tile-green { background: var(--accent-l); }

/* display:block on both, so the name and its subtitle stack whether the view
   builds them from <span>, <div> or <p>. */
.cell-lines { display: block; min-width: 0; }
.act-name {
  display: block;
  font-weight: 700;
  color: var(--ink);
  line-height: 1.35;
  overflow-wrap: anywhere;
}
.act-sub {
  display: block;
  font-size: var(--fs-xs);
  color: var(--muted);
  line-height: 1.45;
  margin-top: 2px;
  overflow-wrap: anywhere;
}

/* The change pill. Tinted red for a deduction and green for a credit, and it
   always carries the coin and the sign, so the meaning does not rest on
   colour alone. */
.chg,
.bal {
  display: inline-flex;
  align-items: center;
  gap: .35rem;
  padding: .25rem .6rem .25rem .45rem;
  border-radius: var(--r-pill);
  font-size: var(--fs-sm);
  font-weight: 700;
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
  line-height: 1.5;
}
.chg-pos { background: var(--ok-bg); color: var(--accent-ink); }
.chg-neg { background: var(--bad-bg); color: var(--danger); }
.bal { background: var(--lav); color: var(--ink); }

.date { color: var(--muted); white-space: nowrap; }

/* ------------------------------------------------------------ empty states */

.empty {
  color: var(--faint);
  text-align: center;
  padding: var(--sp-6) var(--sp-4);
  font-size: var(--fs-sm);
}
td.empty { border-top: 1px solid var(--line); }

.empty-state {
  position: relative;
  border: 1px dashed var(--line-2);
  border-radius: var(--r-md);
  padding: var(--sp-6) var(--sp-5);
  text-align: center;
  color: var(--muted);
  font-size: var(--fs-sm);
  line-height: 1.55;
  background: var(--surface-2);
}
/* A quiet mark rather than an illustration: it says "nothing here yet"
   without pretending to be a picture. */
.empty-state::before {
  content: '';
  display: block;
  width: 34px;
  height: 34px;
  margin: 0 auto var(--sp-3);
  border-radius: 50%;
  border: 2px dashed var(--line-2);
  opacity: .9;
}
.empty-state strong {
  display: block;
  color: var(--ink);
  font-size: var(--fs-lg);
  font-weight: 700;
  margin-bottom: var(--sp-1);
  letter-spacing: 0;
}
/* A call to action inside an empty state gets its own line: inline-flex would
   leave it stranded mid sentence. */
.empty-state .btn {
  display: flex;
  width: fit-content;
  margin: var(--sp-4) auto 0;
}

/* ------------------------------------------------------------ tags */

.tag {
  display: inline-flex;
  align-items: center;
  gap: .35rem;
  padding: .2rem .55rem .2rem .5rem;
  border-radius: var(--r-pill);
  border: 1px solid transparent;
  font-size: var(--fs-2xs);
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: .06em;
  white-space: nowrap;
  line-height: 1.5;
  background: var(--lav);
  color: var(--ink);
  vertical-align: middle;
}
/* The status dot carries the meaning at a glance in a dense queue; the word
   carries it for anyone who cannot see colour. */
.tag::before {
  content: '';
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: currentColor;
  flex: 0 0 auto;
  opacity: .95;
}

.tag-new,
.tag-pending,
.tag-queued,
.tag-signup {
  background: var(--info-bg);
  color: var(--info-ink);
  border-color: var(--info-line);
}

.tag-contacted,
.tag-invited,
.tag-pending_approval,
.tag-pending-approval,
.tag-awaiting {
  background: var(--warn-bg);
  color: var(--warn);
  border-color: var(--warn-line);
}

.tag-processing,
.tag-fulfilled,
.tag-approved,
.tag-joined,
.tag-active,
.tag-sent,
.tag-allowlist {
  background: var(--ok-bg);
  color: var(--accent-ink);
  border-color: var(--ok-line);
}

.tag-rejected,
.tag-cancelled,
.tag-failed,
.tag-disabled {
  background: var(--bad-bg);
  color: var(--danger);
  border-color: var(--bad-line);
}

/* ------------------------------------------------------------ sections & panels */

.section-h {
  font-size: var(--fs-2xs);
  text-transform: uppercase;
  letter-spacing: var(--track-caps);
  color: var(--faint);
  font-weight: 700;
  margin: var(--sp-6) 0 var(--sp-3);
  display: flex;
  align-items: center;
  gap: var(--sp-3);
}
/* A hairline that runs out from the label to the edge. Cheap, and it makes a
   long admin page read as sections rather than one column of stuff. */
.section-h::after {
  content: '';
  flex: 1 1 auto;
  height: 1px;
  background: linear-gradient(90deg, var(--line), transparent);
}

.page-title {
  font-size: var(--fs-xl);
  margin-bottom: var(--sp-4);
  line-height: var(--lh-tight);
}
.panel-title { font-size: var(--fs-lg); font-weight: 700; margin-bottom: var(--sp-2); }

.panel {
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--r-md);
  padding: var(--sp-4);
  box-shadow: var(--shadow-1);
}
.panel + .panel { margin-top: var(--sp-4); }

.stat-row { display: flex; flex-wrap: wrap; gap: var(--sp-5); }
.stat { min-width: 5.5rem; }
.stat .v {
  font-size: var(--fs-2xl);
  font-weight: 700;
  font-variant-numeric: tabular-nums;
  line-height: 1.1;
  color: var(--ink);
}
.stat .v.warn { color: var(--warn); }
.stat .v.accent { color: var(--accent-ink); }
.stat .k {
  font-size: var(--fs-2xs);
  text-transform: uppercase;
  letter-spacing: .08em;
  color: var(--faint);
  font-weight: 700;
  margin-top: var(--sp-2);
}
.stat a { text-decoration: none; }
.stat a:hover .v { color: var(--accent-ink); }

.mono { font-family: var(--mono); font-size: var(--fs-xs); overflow-wrap: anywhere; }

.rowuser { display: flex; align-items: center; gap: var(--sp-2); min-width: 0; }
.rowuser .avatar { width: 30px; height: 30px; font-size: .625rem; }

.kv { display: grid; grid-template-columns: 1fr; gap: 0 var(--sp-3); font-size: var(--fs-sm); margin: 0; }
.kv dt {
  color: var(--faint);
  text-transform: uppercase;
  font-size: var(--fs-2xs);
  letter-spacing: .06em;
  font-weight: 700;
}
.kv dd { margin: 0 0 var(--sp-3); overflow-wrap: anywhere; }

.thumbs { display: flex; flex-wrap: wrap; gap: var(--sp-2); }
.thumbs a {
  display: block;
  border: 1px solid var(--line);
  border-radius: var(--r-sm);
  overflow: hidden;
  line-height: 0;
  transition: border-color var(--dur) var(--ease);
}
.thumbs a:hover { border-color: var(--accent); }
.thumbs img { display: block; width: 72px; height: 72px; object-fit: cover; }

/* ------------------------------------------------------------ flash */

.flash {
  margin-bottom: var(--sp-4);
  padding: var(--sp-3) var(--sp-4);
  border-radius: var(--r-md);
  font-size: var(--fs-sm);
  line-height: 1.5;
  border: 1px solid var(--line);
  border-left-width: 3px;
  display: flex;
  align-items: flex-start;
  gap: var(--sp-3);
  background: var(--surface-2);
  color: var(--ink);
  box-shadow: var(--shadow-1);
}
.flash p { min-width: 0; overflow-wrap: anywhere; }
.flash-success { background: var(--ok-bg); border-color: var(--ok-line); border-left-color: var(--accent); color: var(--ok-ink); }
.flash-error { background: var(--bad-bg); border-color: var(--bad-line); border-left-color: var(--danger); color: var(--bad-ink); }
.flash-warn { background: var(--warn-bg); border-color: var(--warn-line); border-left-color: var(--warn-edge); color: var(--warn-ink); }
.flash-info { background: var(--info-bg); border-color: var(--info-line); border-left-color: var(--info-ink); color: var(--info-deep-ink); }

.flash-dismiss {
  margin-left: auto;
  flex-shrink: 0;
  background: none;
  border: 0;
  color: inherit;
  opacity: .7;
  font-size: 1.125rem;
  line-height: 1;
  cursor: pointer;
  padding: var(--sp-1) .35rem;
  min-width: 34px;
  min-height: 34px;
  border-radius: var(--r-sm);
  font-family: inherit;
  transition: opacity var(--dur) var(--ease), background-color var(--dur) var(--ease);
}
.flash-dismiss:hover { opacity: 1; background: var(--overlay-soft); }

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

.authwrap { max-width: 24rem; margin: 0 auto; padding: var(--sp-6) var(--sp-5) var(--sp-7); width: 100%; }

/* layout.js already puts the auth screens inside .authwrap. If a view wraps its
   own content in it as well, the inner one must not double the padding. */
.authwrap .authwrap { max-width: none; margin: 0; padding: 0; }

/* The lockup layout.js renders above the sign-in card: the wordmark over the
   programme eyebrow, centred, so somebody who has just pointed a phone at a QR
   code in a venue is told in words where they have landed. No symbol here: the
   hub does not permit it next to the wordmark. */
.authmark { display: flex; justify-content: center; margin-bottom: var(--sp-4); }
.authmark .brand { flex-direction: column; gap: var(--sp-3); text-align: center; }
.authmark .brand-lines { align-items: center; }
.authmark .brand-word { font-size: 1.25rem; }
.authmark .brand-eyebrow { font-size: var(--fs-2xs); letter-spacing: .16em; }
.authmark .brand-mark { border-radius: 13px; box-shadow: 0 10px 26px -12px rgba(2, 46, 52, .5); }

/* layout.js renders the lockup above every auth screen, and the views under it
   open with their own "Ideals, Precision Series" eyebrow. Two of them stacked
   on one card reads as a mistake, so the lockup wins and the echo is
   suppressed. No information is lost: those words are still the first thing on
   the page, one line higher and set properly. */
.authmark ~ .authbrand .eyebrow { display: none; }
.authmark ~ .authbrand h1 { margin-top: 0; font-size: var(--fs-2xl); }

.authbrand { text-align: center; margin-bottom: var(--sp-5); }
.eyebrow {
  font-size: var(--fs-2xs);
  letter-spacing: .14em;
  text-transform: uppercase;
  color: var(--accent-ink);
  font-weight: 700;
}
.authbrand h1 { font-size: var(--fs-xl); margin-top: var(--sp-2); }
.authbrand p { color: var(--muted); font-size: var(--fs-md); margin-top: var(--sp-2); line-height: 1.5; }
.authcard {
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--r-lg);
  padding: var(--sp-5) var(--sp-4);
  box-shadow: var(--shadow-2);
}

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

label {
  display: block;
  font-size: var(--fs-sm);
  font-weight: 700;
  margin-bottom: var(--sp-2);
  color: var(--ink);
}

input[type=text],
input[type=email],
input[type=password],
input[type=number],
input[type=search],
input[type=date],
select,
textarea {
  width: 100%;
  padding: var(--sp-3);
  font-family: var(--font);
  font-size: 16px;               /* 16px stops iOS zooming the viewport on focus */
  background: var(--surface);
  border: 1px solid var(--line-2);
  border-radius: var(--r-sm);
  color: var(--ink);
  min-height: var(--tap);
  transition: border-color var(--dur) var(--ease), box-shadow var(--dur) var(--ease);
}
textarea { line-height: 1.5; resize: vertical; }

input::placeholder, textarea::placeholder { color: var(--faint); }
input:hover, select:hover, textarea:hover { border-color: var(--line-3); }
input:focus, select:focus, textarea:focus { border-color: var(--accent); }
input:disabled, select:disabled, textarea:disabled {
  opacity: .6;
  cursor: not-allowed;
  background: var(--surface-2);
}

input[type=file] {
  width: 100%;
  font-family: var(--font);
  font-size: var(--fs-sm);
  color: var(--muted);
  padding: var(--sp-2);
  background: var(--surface);
  border: 1px solid var(--line-2);
  border-radius: var(--r-sm);
  min-height: var(--tap);
}
input[type=file]::file-selector-button {
  font-family: var(--font);
  font-size: var(--fs-xs);
  font-weight: 700;
  color: var(--ink);
  background: var(--surface-2);
  border: 1px solid var(--line-2);
  border-radius: var(--r-sm);
  padding: var(--sp-2) var(--sp-3);
  margin-right: var(--sp-3);
  min-height: 36px;
  cursor: pointer;
  transition: border-color var(--dur) var(--ease), background-color var(--dur) var(--ease);
}
input[type=file]::file-selector-button:hover { border-color: var(--accent); background: var(--accent-l); }

input[type=checkbox], input[type=radio] {
  width: 20px;
  height: 20px;
  accent-color: var(--accent-d);
  vertical-align: middle;
}

[aria-invalid="true"] { border-color: var(--danger) !important; }
[aria-invalid="true"]:focus-visible { outline-color: var(--danger); box-shadow: var(--ring-danger); }

.field { margin-bottom: var(--sp-4); }
.field:last-child { margin-bottom: 0; }
.field-err { font-size: var(--fs-xs); color: var(--danger); margin-top: var(--sp-1); font-weight: 700; }

.checkline { display: flex; align-items: flex-start; gap: var(--sp-3); min-height: var(--tap); }
.checkline label { margin: 0; font-weight: 400; font-size: var(--fs-sm); color: var(--muted); line-height: 1.45; }
.checkline input[type=checkbox] { margin-top: 2px; }

.dropzone {
  border: 1px dashed var(--line-2);
  border-radius: var(--r-md);
  padding: var(--sp-5);
  text-align: center;
  color: var(--muted);
  font-size: var(--fs-sm);
  background: var(--surface-2);
  transition: border-color var(--dur) var(--ease), background-color var(--dur) var(--ease);
}
.dropzone:hover { border-color: var(--accent); background: var(--accent-l); }
.dropzone:focus-within { border-color: var(--accent); }
.dropzone input[type=file] { margin-top: var(--sp-3); text-align: left; }

fieldset { border: 0; padding: 0; margin: 0 0 var(--sp-4); }
legend { padding: 0; font-size: var(--fs-sm); font-weight: 700; margin-bottom: var(--sp-1); }

/* ------------------------------------------------------------ filter bar */

.filterbar {
  display: flex;
  flex-wrap: wrap;
  gap: var(--sp-2);
  align-items: flex-end;
  background: var(--surface-2);
  border: 1px solid var(--line);
  border-radius: var(--r-md);
  padding: var(--sp-3) var(--sp-4);
  margin-bottom: var(--sp-5);
}
.filterbar .field { margin-bottom: 0; flex: 1 1 9rem; min-width: 0; }
.filterbar label {
  font-size: var(--fs-2xs);
  text-transform: uppercase;
  letter-spacing: .06em;
  color: var(--faint);
  font-weight: 700;
  margin-bottom: var(--sp-1);
}
.filterbar .btn { flex: 0 0 auto; }

/* ------------------------------------------------------------ pagination */

.pagination {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--sp-2);
  margin-top: var(--sp-5);
}
.pagination .page-of {
  font-size: var(--fs-xs);
  color: var(--faint);
  margin-right: auto;
  font-variant-numeric: tabular-nums;
}
.pagination a,
.pagination .current,
.pagination .disabled {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: var(--tap);
  min-height: var(--tap);
  padding: 0 var(--sp-3);
  border: 1px solid var(--line-2);
  border-radius: var(--r-sm);
  font-size: var(--fs-sm);
  font-weight: 600;
  text-decoration: none;
  color: var(--ink);
  background: var(--surface);
  transition: border-color var(--dur) var(--ease), background-color var(--dur) var(--ease);
}
.pagination a:hover { border-color: var(--accent); background: var(--accent-l); color: var(--accent-ink); }
.pagination a:active { transform: translateY(1px); }
.pagination .current {
  background: var(--accent-d);
  border-color: var(--accent-d);
  color: var(--on-accent);
  font-weight: 700;
}
.pagination .disabled { opacity: .5; color: var(--faint); background: var(--surface-2); }

/* ------------------------------------------------------------ modal */

.modal-root {
  position: fixed;
  inset: 0;
  z-index: 50;
  display: grid;
  place-items: center;
  background: var(--scrim);
  padding: var(--sp-4);
  overflow-y: auto;
  backdrop-filter: blur(3px);
}
.modal-root[hidden] { display: none; }

.modal {
  background: var(--surface);
  border-radius: var(--r-lg);
  padding: var(--sp-5);
  width: 100%;
  max-width: 29rem;
  max-height: 88vh;
  overflow-y: auto;
  box-shadow: var(--shadow-3);
}
.modal h2 { font-size: var(--fs-lg); font-weight: 700; margin-bottom: var(--sp-2); padding-right: 2.5rem; }
.modal-help { font-size: var(--fs-sm); color: var(--muted); margin-bottom: var(--sp-5); line-height: 1.5; }
.modal-close {
  float: right;
  background: none;
  border: none;
  color: var(--muted);
  font-size: 1.5rem;
  line-height: 1;
  cursor: pointer;
  font-family: inherit;
  min-width: var(--tap);
  min-height: var(--tap);
  border-radius: var(--r-sm);
  margin: -.5rem -.5rem 0 0;
  transition: color var(--dur) var(--ease), background-color var(--dur) var(--ease);
}
.modal-close:hover { color: var(--ink); background: var(--surface-2); }

/* Cancel on the left, the filled green primary on the right. On a phone they
   stack and the primary comes first under the thumb. */
.modal-actions {
  display: flex;
  flex-direction: column-reverse;
  gap: var(--sp-2);
  margin-top: var(--sp-5);
}
.modal-actions .btn { width: 100%; }

/* Set on <body> while a modal is open. No inline styles, the CSP forbids them. */
body.modal-open { overflow: hidden; }

/* ------------------------------------------------------------ callout */

.callout {
  background: var(--ok-bg);
  border: 1px solid var(--ok-line);
  border-left: 3px solid var(--accent);
  border-radius: var(--r-md);
  padding: var(--sp-3) var(--sp-4);
  font-size: var(--fs-sm);
  line-height: 1.55;
  color: var(--ok-ink);
  margin-bottom: var(--sp-5);
}
.callout strong { color: var(--accent-ink); }
.callout p + p { margin-top: var(--sp-2); }

.callout-warn {
  background: var(--warn-bg);
  border-color: var(--warn-line);
  border-left-color: var(--warn-edge);
  color: var(--warn-ink);
}
.callout-warn strong { color: var(--warn); }

.callout-danger {
  background: var(--bad-bg);
  border-color: var(--bad-line);
  border-left-color: var(--danger);
  color: var(--bad-ink);
}
.callout-danger strong { color: var(--danger); }

/* ------------------------------------------------------------ disclosure
   .disclose: the ONE way this product hides an explanation.

   The client's note was that every screen opens with a paragraph explaining
   itself, so the thing you came to do is below four lines of prose you have
   already read. The fix is a native <details>/<summary>: no JavaScript, no
   ARIA to get wrong, nothing for the CSP to block, keyboard operable and
   announced correctly by every screen reader. With this stylesheet missing
   entirely a <details> is still a working disclosure, which is exactly why it
   was chosen over a scripted popover.

   Markup contract, used by views/portal.js and views/admin.js alike:

     <details class="disclose">
       <summary>Why sign-in links show as redacted</summary>
       <div class="disclose-body"> … </div>
     </details>

   A summary may instead hold ONE heading element, which is legal HTML and
   keeps the document outline intact on the guide and the FAQ:

     <summary><h2 class="panel-title">How points work</h2></summary>

   Variants:
     .disclose-plain  no card. For use inside a .callout, .panel or a form
                      field, where a box inside a box reads as a mistake.
     .disclose-lg     panel-sized summary, for a page built of them.

   Rules for whoever writes the next one:
     - Collapsed by default. The one exception is a WARNING whose first
       sentence changes what somebody does in the next thirty seconds: that
       sentence stays on the page and only the rest of it collapses.
     - The summary is a CONTROL, not a heading. It gets the pointer cursor,
       the focus ring and the marker below, and its words say what is inside
       ("Why each person needs their own login"), never "More" or "Details".
     - Never put the only copy of something actionable inside one. A form
       control, a link somebody has to reach, an error: those stay visible. */

.disclose {
  border: 1px solid var(--line);
  border-radius: var(--r-md);
  background: var(--surface);
  margin-top: var(--sp-3);
}
.disclose + .disclose { margin-top: var(--sp-2); }

/* The affordance. flex both lays it out and removes the default triangle in
   Chrome; list-style covers Firefox and the ::-webkit- rule covers Safari. */
.disclose > summary {
  display: flex;
  align-items: center;
  gap: var(--sp-2);
  min-height: var(--tap);
  padding: var(--sp-2) var(--sp-3);
  border-radius: var(--r-md);
  cursor: pointer;
  font-size: var(--fs-sm);
  font-weight: 700;
  line-height: 1.4;
  color: var(--accent-ink);
  list-style: none;
  overflow-wrap: anywhere;
  -webkit-tap-highlight-color: transparent;
}
.disclose > summary::-webkit-details-marker { display: none; }
.disclose > summary:hover { color: var(--accent-dd); background: var(--accent-l); }
.disclose[open] > summary { border-bottom-left-radius: 0; border-bottom-right-radius: 0; }

/* The ring is the global one; it only needs the card's own radius so it does
   not cut the corners off the control it is drawn around. */
.disclose > summary:focus-visible { border-radius: var(--r-md); }

/* The question-mark glyph. Declared twice on purpose: the second form gives
   the generated content an empty accessible name, so a screen reader reads the
   summary's words and not "question mark" fifteen times down a FAQ. A browser
   that does not understand the alt syntax drops that line and keeps the first,
   so the glyph is never lost. */
.disclose > summary::before {
  content: "?";
  content: "?" / "";
  flex: 0 0 auto;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 1.15rem;
  height: 1.15rem;
  border: 1px solid currentColor;
  border-radius: var(--r-pill);
  font-size: var(--fs-2xs);
  font-weight: 700;
  line-height: 1;
}

/* The marker: a chevron that points down when shut and up when open. */
.disclose > summary::after {
  content: "";
  flex: 0 0 auto;
  margin-left: auto;
  width: .42rem;
  height: .42rem;
  border-right: 2px solid currentColor;
  border-bottom: 2px solid currentColor;
  transform: translateY(-.12em) rotate(45deg);
  transition: transform .18s ease;
}
.disclose[open] > summary::after { transform: translateY(.12em) rotate(225deg); }

.disclose-body {
  padding: 0 var(--sp-3) var(--sp-3);
  font-size: var(--fs-sm);
  line-height: 1.55;
  color: var(--muted);
}
.disclose-body > :first-child { margin-top: 0; }
.disclose-body > :last-child { margin-bottom: 0; }
.disclose-body p + p,
.disclose-body p + ul,
.disclose-body p + ol,
.disclose-body ul + p,
.disclose-body .tablewrap { margin-top: var(--sp-2); }
.disclose-body strong { color: var(--ink); }
.disclose-body a { color: var(--accent-ink); }

/* A heading is legal inside a summary and keeps the outline right. It must not
   also look like a heading: the control's own type wins. */
.disclose > summary > h1,
.disclose > summary > h2,
.disclose > summary > h3,
.disclose > summary > h4 {
  font: inherit;
  color: inherit;
  margin: 0;
  min-width: 0;
}

/* ---- .disclose-plain: inside a callout, a panel or a field, where the card
   would be a second box around the same words. */
.disclose-plain {
  border: 0;
  background: none;
  border-radius: 0;
  margin-top: var(--sp-2);
}
.disclose-plain > summary {
  padding: var(--sp-1) 0;
  min-height: 0;
  border-radius: var(--r-sm);
}
.disclose-plain > summary:hover { background: none; text-decoration: underline; }
.disclose-plain > summary:focus-visible { border-radius: var(--r-sm); }
.disclose-plain > .disclose-body { padding: var(--sp-2) 0 0; color: inherit; }

/* ---- .disclose-lg: the guide and the FAQ, where the page IS a stack of these
   and each summary carries the fact that used to be a panel title. */
.disclose-lg > summary {
  padding: var(--sp-3) var(--sp-4);
  font-size: var(--fs-base);
  color: var(--ink);
  gap: var(--sp-3);
}
.disclose-lg > summary::before { width: 1.3rem; height: 1.3rem; font-size: var(--fs-xs); color: var(--accent-ink); }
.disclose-lg > summary:hover { color: var(--accent-ink); }
.disclose-lg > .disclose-body { padding: 0 var(--sp-4) var(--sp-4); font-size: var(--fs-md); }
.disclose-lg + .disclose-lg { margin-top: var(--sp-3); }

/* ---- in a tinted callout the ink has to come from the callout, not the green
   ramp, or the control loses its contrast against the tint. Both variants: an
   amber or red callout is exactly where a green summary would fail AA. */
.callout .disclose { background: none; border-color: var(--overlay-line); }
.callout .disclose > summary,
.callout .disclose > summary:hover { color: inherit; }
.callout .disclose > summary:hover { background: var(--overlay-faint); }
.callout .disclose-plain > summary:hover { background: none; }
.callout .disclose-body { color: inherit; }
.callout-warn .disclose > summary { color: var(--warn); }
.callout-danger .disclose > summary { color: var(--danger); }

/* ---- inside the deep green hero the ramp inverts. */
.hero .disclose,
.hero .disclose-plain > summary { color: var(--hero-ink); }

/* ---- 360px. The summary is a flex row of glyph, words and chevron; only the
   words may shrink, and they wrap rather than push the chevron off the edge. */
.disclose > summary > * { min-width: 0; }
@media (max-width: 22.5rem) {
  .disclose > summary { padding: var(--sp-2); gap: var(--sp-2); }
  .disclose-lg > summary { padding: var(--sp-3); }
  .disclose-body { padding: 0 var(--sp-2) var(--sp-3); }
  .disclose-lg > .disclose-body { padding: 0 var(--sp-3) var(--sp-3); }
}

/* --------------------------------------------------- the appearance control

   Three submit buttons in a segmented control, rendered into the profile menu
   on both surfaces: themeControl() in views/layout.js for the attendee, and the
   same function again in views/admin.js for the concierge. POST /theme records
   the choice. No client script is involved anywhere in it, which is why it keeps
   working with JavaScript switched off.

   WHICH ONE IS ON IS DECIDED HERE, NOT IN THE MARKUP. The form is rendered
   identically on every page and deliberately does not know the current theme.
   The DOCUMENT knows: either it carries the data-theme attribute that
   http/respond.js stamps, or it carries nothing at all, and nothing at all is
   exactly what "follow my device" is. So the three states are read off :root:

     :root[data-theme="dark"]   -> Dark is on
     :root[data-theme="light"]  -> Light is on
     :root:not([data-theme])    -> Auto is on

   The third is the default and the reason this cannot be a class on a button:
   there is no attribute to key off, only its absence.

   THE ACTIVE STATE DOES NOT REST ON COLOUR. It carries a tick, a heavier
   weight and a filled ground with its own edge, so it still reads in greyscale
   and for anyone who cannot separate the green from the grey. */

.themeform {
  display: block;
  margin: var(--sp-2) var(--sp-1) var(--sp-1);
}

.themeform-label {
  display: block;
  padding: 0 var(--sp-2);
  margin-bottom: var(--sp-2);
  font-size: var(--fs-2xs);
  text-transform: uppercase;
  letter-spacing: .06em;
  font-weight: 700;
  color: var(--faint);
}

/* Equal thirds. The active button gains a tick and a heavier weight, and fixed
   columns are what stop the other two shuffling sideways when it does. */
.themeset {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: 2px;
  padding: 2px;
  background: var(--surface-2);
  border: 1px solid var(--line-2);
  border-radius: var(--r-sm);
}

.themebtn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: .4rem;
  min-width: 0;
  min-height: var(--tap);
  padding: var(--sp-2) .3rem;
  border: 1px solid transparent;
  border-radius: 6px;
  background: none;
  font-family: inherit;
  font-size: var(--fs-xs);
  font-weight: 600;
  line-height: 1.2;
  color: var(--muted);
  white-space: nowrap;
  cursor: pointer;
  transition: background-color var(--dur) var(--ease), color var(--dur) var(--ease),
    border-color var(--dur) var(--ease);
}
/* The hover ground comes from the overlay tokens rather than a surface, because
   this control is the one place a chip sits on --surface-2 in both schemes and
   the two schemes want opposite directions: light darkens, dark lightens. */
.themebtn:hover { color: var(--ink); background: var(--overlay-faint); }
.themebtn:active { background: var(--overlay-soft); }

/* The ring is the global one; only its offset moves. .pmenu-panel clips its
   children, and the outer two buttons sit close enough to that edge that 2px of
   offset would have the ring cut in half. */
.themebtn:focus-visible { outline-offset: 1px; border-radius: 6px; }

/* The tick, drawn from two borders so it needs no font and no image. It is on
   EVERY button, transparent until that button is the live one, so choosing a
   different appearance does not shift the three labels sideways. */
.themebtn::before {
  content: '';
  flex: 0 0 auto;
  width: .3rem;
  height: .55rem;
  margin-top: -.15rem;
  border-right: 2px solid currentColor;
  border-bottom: 2px solid currentColor;
  transform: rotate(45deg);
  opacity: 0;
  transition: opacity var(--dur) var(--ease);
}

/* The three live states. Same declarations, three ways of being true. */
:root[data-theme="dark"] .themebtn[data-theme-opt="dark"],
:root[data-theme="light"] .themebtn[data-theme-opt="light"],
:root:not([data-theme]) .themebtn[data-theme-opt="system"] {
  background: var(--accent-l);
  border-color: var(--accent-l2);
  color: var(--accent-ink);
  font-weight: 700;
}
:root[data-theme="dark"] .themebtn[data-theme-opt="dark"]::before,
:root[data-theme="light"] .themebtn[data-theme-opt="light"]::before,
:root:not([data-theme]) .themebtn[data-theme-opt="system"]::before {
  opacity: 1;
}
/* Without this the live button would be the one control in the product that
   does not answer the pointer: the rules above outrank .themebtn:hover. */
:root[data-theme="dark"] .themebtn[data-theme-opt="dark"]:hover,
:root[data-theme="light"] .themebtn[data-theme-opt="light"]:hover,
:root:not([data-theme]) .themebtn[data-theme-opt="system"]:hover {
  background: var(--accent-l2);
  color: var(--accent-ink);
}

/* ------------------------------------------------------------ footer */

.foot {
  display: flex;
  flex-wrap: wrap;
  gap: var(--sp-2) var(--sp-4);
  justify-content: center;
  align-items: center;
  padding: var(--sp-4) var(--sp-4) var(--sp-5);
  border-top: 1px solid var(--line);
  font-size: var(--fs-xs);
  background: var(--surface-2);
}
.foot form { margin: 0; display: inline-flex; }
.foot a,
.foot .linkbtn {
  color: var(--muted);
  text-decoration: none;
  font-size: var(--fs-xs);
  display: inline-flex;
  align-items: center;
  min-height: var(--tap);
  padding: 0 var(--sp-1);
  transition: color var(--dur) var(--ease);
}
.foot a:hover,
.foot .linkbtn:hover { color: var(--accent-ink); }
.foot a[aria-current="page"] { color: var(--ink); font-weight: 700; }

/* ------------------------------------------------------------ long-form copy */

.prose { max-width: 42rem; }
.prose h2 { font-size: var(--fs-lg); margin: var(--sp-6) 0 var(--sp-2); }
.prose h3 { font-size: var(--fs-base); margin: var(--sp-5) 0 var(--sp-1); }
.prose p { color: var(--muted); font-size: var(--fs-md); margin: var(--sp-2) 0; line-height: 1.65; }
.prose ul, .prose ol { color: var(--muted); font-size: var(--fs-md); padding-left: 1.15rem; margin: var(--sp-2) 0; line-height: 1.65; }
.prose li { margin: var(--sp-1) 0; }
.prose li::marker { color: var(--faint); }
.prose strong { color: var(--ink); }
.prose a { color: var(--accent-ink); text-underline-offset: 2px; }
.prose a:hover { color: var(--accent-dd); }
.prose code { font-family: var(--mono); font-size: var(--fs-sm); color: var(--accent-ink); }

.list { color: var(--muted); font-size: var(--fs-md); padding-left: 1.1rem; margin: var(--sp-2) 0; line-height: 1.6; }
.list li { margin: var(--sp-1) 0; }
.list li::marker { color: var(--faint); }

/* ------------------------------------------------------------ error page

   Served by http/respond.js with no layout and no client script, so everything
   it needs is here: the mark is drawn as a background image rather than markup.
   Same geometry AND the same two colours as views/icons.js MARK_PATHS, which
   is the placeholder pending the official logo file. If that changes, change
   this. */

.page-error {
  display: grid;
  place-items: center;
  min-height: 100vh;
  background: var(--bg);
}
@supports (min-height: 100dvh) {
  .page-error { min-height: 100dvh; }
}
.page-error .wrap {
  max-width: 32rem;
  margin: 0 auto;
  padding: var(--sp-7) var(--sp-5);
  text-align: center;
  flex: 0 1 auto;
  background: transparent;
}
.page-error .wrap::before {
  content: '';
  display: block;
  width: 52px;
  height: 52px;
  margin: 0 auto var(--sp-5);
  border-radius: 50%;
  box-shadow: 0 12px 28px -14px rgba(2, 46, 52, .45);
  /* The official Ideals mark, the same artwork as views/icons.js MARK_PATHS.
     Inlined because this page is served with no layout and no script. */
  background-image: url("data:image/svg+xml,%3Csvg width=%22160%22 height=%22160%22 viewBox=%220 0 160 160%22 fill=%22none%22 xmlns=%22http://www.w3.org/2000/svg%22%3E%3Cpath d=%22M80 160C124.183 160 160 124.183 160 80C160 35.8172 124.183 0 80 0C35.8172 0 0 35.8172 0 80C0 124.183 35.8172 160 80 160Z%22 fill=%22%231D8269%22/%3E%3Cpath d=%22M80 160C124.183 160 160 124.183 160 80C160 35.8172 124.183 0 80 0C35.8172 0 0 35.8172 0 80C0 124.183 35.8172 160 80 160Z%22 fill=%22url(%23im-paint0_radial_530_3030)%22 fill-opacity=%220.9%22/%3E%3Cpath d=%22M60 150C93.1371 150 120 118.66 120 80C120 41.3401 93.1371 10 60 10C26.8629 10 0 41.3401 0 80C0 118.66 26.8629 150 60 150Z%22 fill=%22url(%23im-paint1_linear_530_3030)%22/%3E%3Cpath d=%22M60 150C93.1371 150 120 118.66 120 80C120 41.3401 93.1371 10 60 10C26.8629 10 0 41.3401 0 80C0 118.66 26.8629 150 60 150Z%22 fill=%22url(%23im-paint2_radial_530_3030)%22 fill-opacity=%220.9%22/%3E%3Cpath d=%22M40 140C62.0914 140 80 113.137 80 80C80 46.8629 62.0914 20 40 20C17.9086 20 0 46.8629 0 80C0 113.137 17.9086 140 40 140Z%22 fill=%22url(%23im-paint3_linear_530_3030)%22/%3E%3Cpath d=%22M40 140C62.0914 140 80 113.137 80 80C80 46.8629 62.0914 20 40 20C17.9086 20 0 46.8629 0 80C0 113.137 17.9086 140 40 140Z%22 fill=%22url(%23im-paint4_radial_530_3030)%22/%3E%3Cpath d=%22M20 120C31.0457 120 40 102.091 40 80C40 57.9086 31.0457 40 20 40C8.95431 40 0 57.9086 0 80C0 102.091 8.95431 120 20 120Z%22 fill=%22%2357D188%22/%3E%3Cpath d=%22M20 120C31.0457 120 40 102.091 40 80C40 57.9086 31.0457 40 20 40C8.95431 40 0 57.9086 0 80C0 102.091 8.95431 120 20 120Z%22 fill=%22url(%23im-paint5_linear_530_3030)%22 fill-opacity=%220.85%22/%3E%3Cdefs%3E%3CradialGradient id=%22im-paint0_radial_530_3030%22 cx=%220%22 cy=%220%22 r=%221%22 gradientUnits=%22userSpaceOnUse%22 gradientTransform=%22translate(60 98.75) rotate(-90) scale(88.75 75.4461)%22%3E%3Cstop offset=%220.585%22 stop-color=%22%23022E34%22/%3E%3Cstop offset=%221%22 stop-color=%22%23022E34%22 stop-opacity=%220%22/%3E%3C/radialGradient%3E%3ClinearGradient id=%22im-paint1_linear_530_3030%22 x1=%2260%22 y1=%22-50.8333%22 x2=%2260%22 y2=%22150%22 gradientUnits=%22userSpaceOnUse%22%3E%3Cstop stop-color=%22%232C9C74%22/%3E%3Cstop offset=%221%22 stop-color=%22%233FB67D%22/%3E%3C/linearGradient%3E%3CradialGradient id=%22im-paint2_radial_530_3030%22 cx=%220%22 cy=%220%22 r=%221%22 gradientUnits=%22userSpaceOnUse%22 gradientTransform=%22translate(40 99.1667) rotate(-90) scale(78.3974 51.9561)%22%3E%3Cstop offset=%220.485%22 stop-color=%22%23022E34%22/%3E%3Cstop offset=%221%22 stop-color=%22%23022E34%22 stop-opacity=%220%22/%3E%3C/radialGradient%3E%3ClinearGradient id=%22im-paint3_linear_530_3030%22 x1=%2240%22 y1=%22-27.5%22 x2=%2240%22 y2=%22140%22 gradientUnits=%22userSpaceOnUse%22%3E%3Cstop stop-color=%22%232C9C74%22/%3E%3Cstop offset=%221%22 stop-color=%22%2357D188%22/%3E%3C/linearGradient%3E%3CradialGradient id=%22im-paint4_radial_530_3030%22 cx=%220%22 cy=%220%22 r=%221%22 gradientUnits=%22userSpaceOnUse%22 gradientTransform=%22translate(18.3333 93.3333) rotate(-86.1575) scale(55.9591 29.3062)%22%3E%3Cstop offset=%220.350058%22 stop-color=%22%23022E34%22/%3E%3Cstop offset=%220.95%22 stop-color=%22%23022E34%22 stop-opacity=%220%22/%3E%3C/radialGradient%3E%3ClinearGradient id=%22im-paint5_linear_530_3030%22 x1=%2220%22 y1=%22-33.3333%22 x2=%2220%22 y2=%22120%22 gradientUnits=%22userSpaceOnUse%22%3E%3Cstop stop-color=%22%232C9C74%22/%3E%3Cstop offset=%221%22 stop-color=%22%2370EB94%22/%3E%3C/linearGradient%3E%3C/defs%3E%3C/svg%3E");
  background-size: contain;
  background-repeat: no-repeat;
}
.page-error h1 { font-size: var(--fs-2xl); margin: var(--sp-2) 0 var(--sp-3); }
.page-error .muted { font-size: var(--fs-md); line-height: 1.6; }
.page-error .btn { margin-top: var(--sp-5); }

/* ------------------------------------------------------------ utilities

   The CSP forbids style attributes, so the handful of one-off adjustments the
   views need live here instead. Keep this list short. */

.muted { color: var(--muted); }
.faint { color: var(--faint); }
.accent { color: var(--accent-ink); }
.warn { color: var(--warn); }
.danger { color: var(--danger); }
.strong { font-weight: 700; }
.small { font-size: var(--fs-sm); }

.u-full { width: 100%; }
.u-center { text-align: center; }
.u-right { text-align: right; }
.u-grow { flex: 1 1 auto; }
.u-nowrap { white-space: nowrap; }
.u-break { overflow-wrap: anywhere; }

.u-mt-0 { margin-top: 0; }
.u-mt-sm { margin-top: var(--sp-2); }
.u-mt { margin-top: var(--sp-4); }
.u-mt-lg { margin-top: var(--sp-6); }
.u-mb-0 { margin-bottom: 0; }
.u-mb-sm { margin-bottom: var(--sp-2); }
.u-mb { margin-bottom: var(--sp-4); }
.u-mb-lg { margin-bottom: var(--sp-6); }

.stack { display: flex; flex-direction: column; gap: var(--sp-4); }
.stack-sm { display: flex; flex-direction: column; gap: var(--sp-2); }
.row { display: flex; flex-wrap: wrap; gap: var(--sp-4); align-items: center; }
.row-between { display: flex; flex-wrap: wrap; gap: var(--sp-4); align-items: center; justify-content: space-between; }

/* ---- the concierge tables

   A concierge table holds two kinds of cell and they want opposite things.

   MACHINE TOKENS: an action name (auth.signin), a target (session #2), a
   template name (points_credited), an IP address. None of them has a space in
   it, so a column narrow enough to force a break puts it back as
   "auth.signi / n" and the row grows to three lines. The audit log shipped like
   that, and an audit log exists to be scanned. Those cells carry .nowrap in the
   view and never break; they set their column's width, and the table asks the
   wrapper for the room rather than taking it out of the token.

   PROSE: a date, an address, a company, a subject line, a review note. These
   wrap, and they are where the surplus width comes off.

   break-word, not anywhere. `anywhere` removes the longest word from a column's
   min-content width, which is what let the layout squeeze these cells to a
   letter a line in the first place. `break-word` wraps identically when a word
   genuinely cannot fit its own line, but still asks for the whole word while
   the room exists — so an address is split only when there was no other way to
   show it, never merely because a neighbouring column wanted the space.

   The CELL is what is targeted, never a .mono span sitting inside a paragraph.
   A .u-break span inside a cell keeps `anywhere` too: it is marking one value
   known to be unbreakable, a delivery error or a pasted link in a ledger note,
   rather than governing how a whole column is sized. */
.page-admin .tablewrap td.mono,
.page-admin .tablewrap td.u-break {
  overflow-wrap: break-word;
  word-break: normal;
}

/* A prose column is the one the table takes its surplus width out of, and left
   unbounded it is taken all the way down to the longest single word in it:
   "Corrected -50, duplicate award at the simulator" came back as six lines in a
   column the width of "simulator", in a row whose other five cells were one
   line. A floor of 11rem is roughly seven words a line, which is a readable
   measure and keeps the rows to a consistent two lines at worst. The table asks
   the wrapper for the difference; that is what the wrapper is for. */
.page-admin .tablewrap td.u-break { min-width: 11rem; }

/* A cell that holds ONE value, and is not allowed to break it.

   The staff table sent back by the client had "Esther Deona" over two lines,
   "Not joined yet" over three and an address broken after a dot, because the
   auto layout was free to take width from any column that would still wrap and
   these were the columns that would. A person's name read as "Esther / Deona"
   is two people and an address broken mid-domain is a different address, so
   these cells simply do not wrap and the table asks .tablewrap for the width
   that costs.

   td.nowrap already exists and does this. What is added here is the inner span:
   person() marks the name .u-break and .mono carries overflow-wrap: anywhere, so
   the value is put back to normal wrapping rules INSIDE a nowrap cell rather
   than left carrying an instruction that contradicts the one on the cell. */
.page-admin .tablewrap td.nowrap .u-break,
.page-admin .tablewrap td.mono.nowrap {
  overflow-wrap: normal;
  word-break: normal;
}

/* The badge, then the explanation under it. Never side by side.

   A status cell that sets a coloured badge and a full sentence on the same line
   reads as one run of text, and it was making staff rows three times taller than
   the badge needs. The badges sit together on the first line; anything else drops
   below as small muted type, on its own line, in the same voice as .hint. Used
   for a FACT about the row (when an invitation lapses, that no password is set)
   and, in the actions column, for the one short line explaining a control that is
   switched off. Long explanation belongs in the .disclose under the table. */
.cell-note {
  display: block;
  margin-top: var(--sp-1);
  font-size: var(--fs-xs);
  line-height: 1.45;
  color: var(--faint);
  white-space: normal;
}
.cell-note:first-child { margin-top: 0; }
.tagrow { display: flex; flex-wrap: wrap; align-items: center; gap: var(--sp-1); }
/* Both cells hold a short line of prose under their controls, so they get the
   same measure as any other prose column rather than being squeezed to the width
   of their longest word. */
.page-admin .tablewrap td.col-status { min-width: 11rem; }

/* Actions gets a wider floor than Status because it holds CONTROLS, and a
   control is a fixed object that cannot be narrowed by wrapping it. At 11rem
   the cell was squeezed to about seven characters a line and the short line
   under the buttons ran to three, which is what made the first row of the staff
   table half as tall again as the row below it. 14.5rem, padding included, is
   the widest floor the 64rem shell can pay for out of the columns' slack: it
   holds the buttons at their natural size and the line under them to two.

   It is NOT wide enough to sit "Change level" and "Remove access" side by side.
   That needs about 16.5rem, which takes the table past the shell and puts a
   permanent scrollbar under every staff table at every desk. One stacked pair
   in the one row that has it beats a scrollbar on all of them, and it matches
   the row below, where a select and two buttons stack whatever the width. */
.page-admin .tablewrap td.col-actions { min-width: 14.5rem; }
.page-admin .tablewrap td.col-actions .btn-row + .cell-note { margin-top: var(--sp-2); }

/* A row whose Status or Actions cell runs to two or three lines is a row whose
   other four cells hold one line each. Centred vertically, that one line floats
   in the middle of a tall row and stops lining up with the badge it belongs to;
   the eye then has to re-find the row on every column. Top alignment puts the
   name, the address, the level, the date and the badge on the same first line,
   and lets the explanation hang below where it reads as secondary. Scoped to
   rows that actually carry one of those two cells, so the short queue tables are
   left centred as they were. */
.page-admin .tablewrap tr:has(> td.col-status) > td,
.page-admin .tablewrap tr:has(> td.col-actions) > td { vertical-align: top; }

/* An honest floor under the table, and the wrapper scrolls beneath it.

   .tablewrap has been overflow-x: auto since it was written. What it never had
   was a table wide enough to trigger it: with nothing holding the table open,
   six columns simply shared the width of three and every cell in them was
   crushed. The floor scales with the column count, because the column count is
   the squeeze.

   Every floor is at or under 61rem, which is the widest a table can be inside
   the narrow 64rem shell without scrolling, so no concierge table scrolls at a
   desk merely because of this rule. One scrolls only when its own contents need
   more room than the shell has, an IPv6 address in the From column say, which is
   the right trade: a horizontal swipe loses nothing, a broken address does.

   dataTable() puts tw-scroll on any table of four columns or more. The :has()
   rules refine the floor by column count and also catch a table whose view
   built its own markup and forgot the class; a browser without :has() still
   gets the tw-scroll floor rather than nothing.

   Excluded: a table that STACKS on a phone (tw-stack, tw-ledger) has no columns
   left to hold a floor open, and 58rem inside a 360px phone would push the row
   grid off the screen edge. */
.page-admin .tablewrap.tw-scroll table,
.page-admin .tablewrap:not(.tw-stack):not(.tw-ledger) table:has(thead th:nth-child(4)) {
  min-width: 40rem;
}
.page-admin .tablewrap:not(.tw-stack):not(.tw-ledger) table:has(thead th:nth-child(5)) { min-width: 48rem; }
.page-admin .tablewrap:not(.tw-stack):not(.tw-ledger) table:has(thead th:nth-child(6)) { min-width: 56rem; }
.page-admin .tablewrap:not(.tw-stack):not(.tw-ledger) table:has(thead th:nth-child(7)) { min-width: 61rem; }

/* A table that scrolls has to say so. macOS hides its overlay scrollbar until
   something moves, so a concierge reading the audit log has no way of knowing
   that From continues past the right-hand edge; asking for a scrollbar with a
   height turns it into a permanent one, and it still only appears on the
   wrappers that actually overflow. */
.page-admin .tablewrap { scrollbar-width: thin; scrollbar-color: var(--line) transparent; }
.page-admin .tablewrap::-webkit-scrollbar { height: 12px; }
.page-admin .tablewrap::-webkit-scrollbar-track { background: transparent; }
.page-admin .tablewrap::-webkit-scrollbar-thumb {
  background: var(--line);
  border-radius: var(--r-pill);
  border: 3px solid var(--surface);
}
.page-admin .tablewrap:hover::-webkit-scrollbar-thumb { background: var(--faint); }

/* ------------------------------------------------------------ breakpoints */

/* A concierge queue crushed into 360px turns an attendee's name into one
   letter per line. Give the admin tables a floor and let .tablewrap scroll,
   which is what it is for. The attendee tables are three short columns and
   read perfectly at 360, so they are left alone. */
@media (max-width: 40rem) {
  /* The floors for four columns and up are no longer written here: they hold at
     every width now, because a six column table does not fit a 976px shell
     either. This is the three column case, which needs a floor on a phone and
     none at a desk.

     A table that STACKS is excluded, because it has no columns left to give a
     floor to: 30rem inside a 360px phone would put the row grid off the edge.
     A table already carrying tw-scroll is excluded too, so that on a browser
     without :has() this narrower floor does not win on specificity and undo
     the wider one. */
  .page-admin .tablewrap:not(.tw-stack):not(.tw-ledger):not(.tw-scroll) table { min-width: 30rem; }

  /* The attendee's own submissions table carries a second line under the
     activity name: the post link, or "1 photo uploaded", or who was invited.
     Auto layout answers that by widening the first column until Points is
     pushed off the right-hand edge. .tablewrap scrolls, so nothing is lost,
     but a points column you have to swipe sideways to see is the wrong column
     to hide on the one screen that is entirely about points.

     Fixed layout with declared proportions makes the three columns share the
     width they actually have. Everything wraps rather than truncating, because
     the long values here (a URL, a colleague's address) are exactly the ones
     that must break anywhere. */
  .page-portal .tablewrap table { table-layout: fixed; width: 100%; }
  .page-portal .tablewrap th,
  .page-portal .tablewrap td {
    padding: var(--sp-3) var(--sp-2);
    /* break-word, not anywhere: a word is broken only when it cannot fit a line
       on its own. "unchanged" on the guide table fits, and `anywhere` was
       breaking it into "unchange / d" for no reason. */
    overflow-wrap: break-word;
  }
  /* The first column WAS the exception, on the grounds that it holds values a
     person typed: a pasted URL, a colleague's address. But it is also the Step
     column of the guide's "points come off at processing" table, where
     `anywhere` split "Acknowledgement" into "Acknowledgeme / nt" in a 135px
     cell — the exact defect the break-word rule above was written to stop.

     So the exception is opted into by the tables that genuinely need it rather
     than applied to every first column. tw-break is on the tables that carry
     attendee-typed values; the guide's prose table is left on break-word. */
  .page-portal .tablewrap.tw-break td:first-child { overflow-wrap: anywhere; }
  .page-portal .tablewrap td .u-break { overflow-wrap: anywhere; }
  /* 50/28/22 starved the last column: on the guide table it left about 58px,
     which is not enough for the word "unchanged" and broke it as "unchan /
     ged". 44/28/28 takes the room off the first column, which holds a sentence
     and can afford it, and leaves the middle column exactly as it was so the
     status pill on the submissions table is untouched. */
  .page-portal .tablewrap th:first-child,
  .page-portal .tablewrap td:first-child { width: 42%; }
  .page-portal .tablewrap th:last-child,
  .page-portal .tablewrap td:last-child { width: 30%; }
  .page-portal .tablewrap th { white-space: normal; }

  /* Those proportions work for a three column table of short values. Three
     tables are not that, and all of them break at 360px in the same way: a cell
     whose contents will not wrap, and no width to give it.

       · Balance history has FOUR columns. Measured at the 360px design floor,
         the activity column needs about 140px to stay a sentence, the two
         pills about 140px between them and the date about 78px, which is more
         width than the phone has.
       · The concierge reads the SAME four column balance history on the
         attendee page, inside a 30rem admin floor, which is worse: the activity
         column collapses to one letter per line.
       · Team has three, but the middle one is a full corporate email address
         set in the mono face and the last is a status pill that will not wrap.
         At 22% the pill is simply cut off at the right-hand edge.

     Both become a STACK on a phone rather than a table that scrolls sideways,
     because the columns being pushed off the edge are the ones the reader came
     for: the balance they were left with, and whether their colleague joined.

     display:block strips a table of its roles in the accessibility tree, so
     any table using these rules must restate them in its markup:
     role="table" / "rowgroup" / "row" / "columnheader" / "cell". Without that
     the stacked rows stop announcing which figure is which.
     scripts/verify.js §21 asserts it for Balance history. */

  /* ---- shared: stop being a table ---- */
  .tablewrap.tw-stack,
  .tablewrap.tw-ledger,
  .page-portal .tablewrap:has(table thead th:nth-child(4)) { overflow-x: visible; }

  .tablewrap.tw-stack table,
  .tablewrap.tw-ledger table,
  .page-portal .tablewrap:has(table thead th:nth-child(4)) table {
    display: block;
    width: 100%;
    /* .page-admin gives its tables a 30rem floor so a queue does not crush a
       name. A stacked table has no columns left to crush and must not inherit
       it, or the row grid is 480px wide inside a 360px phone. */
    min-width: 0;
    table-layout: auto;
  }
  .tablewrap.tw-stack tbody,
  .tablewrap.tw-ledger tbody,
  .page-portal .tablewrap:has(table thead th:nth-child(4)) tbody { display: block; }

  /* The headers stay in the document for a screen reader and leave the screen
     for everybody else: a repeated "Balance change" label above every pill is
     noise, and the sign and the tint already say it. */
  .tablewrap.tw-stack thead,
  .tablewrap.tw-ledger thead,
  .page-portal .tablewrap:has(table thead th:nth-child(4)) thead {
    position: absolute;
    width: 1px;
    height: 1px;
    margin: -1px;
    padding: 0;
    overflow: hidden;
    clip-path: inset(50%);
    white-space: nowrap;
    border: 0;
  }

  .tablewrap.tw-stack td,
  .tablewrap.tw-ledger td,
  .page-portal .tablewrap:has(table thead th:nth-child(4)) td {
    display: block;
    padding: 0;
    border: 0;
    width: auto;
  }
  /* The zebra stripe is painted on the cells, which fill a table row and do not
     fill a grid one. Moved to the row here, or a stacked row is tinted in
     patches with white gaps between them. */
  .tablewrap.tw-stack tbody tr:nth-child(even),
  .tablewrap.tw-ledger tbody tr:nth-child(even),
  .page-portal .tablewrap:has(table thead th:nth-child(4)) tbody tr:nth-child(even) {
    background: var(--surface-alt);
  }
  .tablewrap.tw-stack tbody tr:nth-child(even) td,
  .tablewrap.tw-ledger tbody tr:nth-child(even) td,
  .page-portal .tablewrap:has(table thead th:nth-child(4)) tbody tr:nth-child(even) td,
  .tablewrap.tw-stack tbody tr:hover td,
  .tablewrap.tw-ledger tbody tr:hover td,
  .page-portal .tablewrap:has(table thead th:nth-child(4)) tbody tr:hover td {
    background: transparent;
  }
  .tablewrap.tw-stack tbody tr:hover,
  .tablewrap.tw-ledger tbody tr:hover,
  .page-portal .tablewrap:has(table thead th:nth-child(4)) tbody tr:hover {
    background: var(--surface-2);
  }
  .tablewrap.tw-stack tbody tr:last-child,
  .tablewrap.tw-ledger tbody tr:last-child,
  .page-portal .tablewrap:has(table thead th:nth-child(4)) tbody tr:last-child {
    border-bottom: 0;
  }
  /* An empty table renders one cell spanning every column. Grid ignores
     colspan, so it is told to span here instead. */
  .tablewrap.tw-stack td[colspan],
  .tablewrap.tw-ledger td[colspan],
  .page-portal .tablewrap:has(table thead th:nth-child(4)) td[colspan] {
    grid-column: 1 / -1;
    justify-self: stretch;
    text-align: left;
  }

  /* ---- tw-ledger: Balance history, on both the attendee tab and the
     concierge attendee page, which render it class for class ----
     The activity takes the whole first line so its note stays a sentence, then
     the change, the balance it left and the date share the line beneath. The
     date closes on the right, where the eye already expects it from the
     desktop table. */
  .tablewrap.tw-ledger tr,
  .page-portal .tablewrap:has(table thead th:nth-child(4)) tr {
    display: grid;
    grid-template-columns: auto auto 1fr;
    align-items: center;
    gap: var(--sp-2);
    padding: var(--sp-4) var(--sp-3);
    border-bottom: 1px solid var(--line);
  }
  .tablewrap.tw-ledger td:first-child,
  .page-portal .tablewrap:has(table thead th:nth-child(4)) td:first-child {
    grid-column: 1 / -1;
  }
  .tablewrap.tw-ledger td:last-child,
  .page-portal .tablewrap:has(table thead th:nth-child(4)) td:last-child {
    justify-self: end;
    text-align: right;
  }

  /* ---- tw-stack: the generic shape, currently Team ----
     The lead cell and the trailing cell share the first line, and anything
     between them gets a line of its own. On Team that reads as the person's
     name with their status against it, and their address underneath, which is
     the order somebody scans a list of colleagues in. */
  .tablewrap.tw-stack tr {
    display: grid;
    grid-template-columns: 1fr auto;
    align-items: center;
    gap: var(--sp-1) var(--sp-3);
    padding: var(--sp-4) var(--sp-3);
    border-bottom: 1px solid var(--line);
  }
  .tablewrap.tw-stack td:last-child:not(:first-child) {
    grid-row: 1;
    grid-column: 2;
    justify-self: end;
    text-align: right;
    /* The three-column width rule above still applies inside this grid row and
       collapsed the status cell to about 25px while the pill inside it was 82px
       wide, so /team overflowed the viewport by 30px and the badges read
       "INVIT" and "JOIN". A grid row sizes its own columns; the table widths
       have no business here. */
    width: auto;
    min-width: 0;
    white-space: nowrap;
  }
  .tablewrap.tw-stack th,
  .tablewrap.tw-stack td { width: auto; }
  .tablewrap.tw-stack td:first-child { min-width: 0; }
  .tablewrap.tw-stack td:not(:first-child):not(:last-child) {
    grid-column: 1 / -1;
  }

  /* The tile stays. It had to go when four columns fought over 360px, but a
     stacked row gives the activity the whole first line, and the reference
     design carries the tile on every row. */
}

/* Small phones: 360px is the design floor. Nothing may overflow it. */
@media (max-width: 22.5rem) {
  .brand-word { font-size: 1rem; }
  .wrap { padding: var(--sp-3) var(--sp-3) var(--sp-6); }
  .hero { padding: var(--sp-4) var(--sp-3); }
  .hero-hi { font-size: var(--fs-lg); }
  .tab { padding: var(--sp-3) var(--sp-2); font-size: var(--fs-sm); }
  .stat-row { gap: var(--sp-4); }
  .card-art { min-height: 148px; padding: var(--sp-5) var(--sp-4); }
}

/* On a phone the panels sit over the discs, so the motif is pulled back to the
   top right corner where it reads as a highlight rather than clutter. */
/* Four tabs have to fit 360px without a swipe, because a tab a thumb cannot
   see is a tab nobody visits. Tightening the padding and dropping to 13px
   buys the room; the row still scrolls if a fifth one is ever added. */
@media (max-width: 40rem) {
  .tabs { gap: 0; }
  .tab { padding: var(--sp-3) .45rem; font-size: var(--fs-sm); }

  /* The concierge nav WRAPS on a phone instead of scrolling sideways.
     853px of nav inside 358px of screen put six of the eight sections off the
     right-hand edge, with the scrollbar suppressed, so the highlighted current
     tab was invisible and nothing hinted that anything was there. admin.js
     scrolls it into view, but that is the enhancement, and a nav that only
     works when the script arrives is the same failure as a card button that
     only works when the script arrives. Wrapped, every section is visible with
     no script at all; the scroll behaviour is kept above this width, where the
     nav is one line and scrolling is the right answer. */
  .adminnav {
    flex-wrap: wrap;
    overflow-x: visible;
    row-gap: 0;
  }
  .adminnav a { padding: var(--sp-2) var(--sp-2); }
}

@media (max-width: 40rem) {
  .hero::before {
    top: -7rem; right: -6.5rem; width: 16rem; height: 16rem; margin-top: 0; opacity: .7;
  }
  .hero::after {
    top: -4.5rem; right: -2.5rem; width: 9rem; height: 9rem; margin-top: 0; opacity: .62;
  }
}

@media (min-width: 40rem) {
  .crumbs { display: flex; }
  .hero-cards { grid-template-columns: 1fr 1fr; }
  .grid { grid-template-columns: repeat(3, minmax(0, 1fr)); }
  .grid-2 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .wrap { padding: var(--sp-5) var(--sp-5) var(--sp-6); }
  .kv { grid-template-columns: 12rem 1fr; }
  .kv dd { margin-bottom: var(--sp-1); }
  .cols { display: grid; grid-template-columns: 1fr 1fr; gap: 0 var(--sp-4); }
  .hero { padding: var(--sp-6); padding-right: 8rem; }
  .hero-hi { font-size: 1.5rem; }
  .panel { padding: var(--sp-5); }
  .authcard { padding: var(--sp-5); }
  .tab { flex: 0 0 auto; }
  .modal { padding: var(--sp-6); }
  .modal-actions { flex-direction: row; justify-content: flex-end; }
  .modal-actions .btn { width: auto; min-width: 7rem; }
}

/* Wide enough for the discs to have the right-hand third to themselves, which
   is the proportion the reference uses. */
@media (min-width: 56rem) {
  .hero { padding-right: 14rem; }
  .hero::after { right: 3rem; }
}

/* Compact buttons are only shrunk where a mouse is driving them. */
@media (min-width: 40rem) and (pointer: fine) {
  .btn-sm { min-height: 34px; }
  .icon-btn::after { inset: 0; }
}

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

@media (prefers-reduced-motion: reduce) {
  * {
    transition: none !important;
    animation: none !important;
    scroll-behavior: auto !important;
  }
  /* The spinner cannot spin, so the busy button says so with a solid ring
     instead of vanishing into nothing. */
  .btn[aria-busy="true"] { color: inherit; opacity: .6; }
  .btn[aria-busy="true"]::before { display: none; }
}
