/* ==========================================================================
   SportSyncs design system

   The visual language is taken from the sport rather than from admin-panel
   convention: the team sheet, the attendance register and the form guide.
   Two devices carry it and are used everywhere:

     .strip   a row of small cells - a form guide (W/D/L) or a register
              (present/absent). It shows a sequence at a glance, which is
              exactly what a coach reads first.
     .spine   a coloured left edge on a row or card, encoding status. It
              replaces a column of badges with something scannable.

   Everything else stays quiet so those two read clearly.

   ---------------------------------------------------------------- breakpoints

   Three, and only three. They were five - 1080, 900, 760, 720 and 700 - which
   was not a scale but a record of the order the pages were written in. Nobody
   chose 720 over 700; two people solved the same problem forty pixels apart
   on different afternoons.

   Five values also produced a real bug rather than only untidiness. The
   sidebar goes off-canvas at 760, handing the content about sixty pixels
   back, but the player hero did not reflow until 700 and the package matrix
   not until 720 - so between 700 and 760 the rail had collapsed and the
   things that should have used the space had not. Merging both into 760 ties
   the reflow to the event that causes it.

     1080px  Density. Multi-column grids drop to two columns; the split
             auth layout stacks. Everything is still a desktop.
     900px   The chrome starts giving way. The user chip loses its name,
             the sign-in artwork goes.
     760px   Phone. The sidebar becomes an overlay, tables stack into
             cards, heroes and the matrix reflow.

   Add a fourth only with a reason that is not "this one page looks slightly
   better at 840". A breakpoint is a promise that every screen changes shape
   together, and each new one is a fourth layout to check.
   ========================================================================== */

:root {
  /* --------------------------------------------------------------- palette
     Dark navy chrome, electric blue for action, cyan for performance.

     Two values per outcome colour, and the reason matters. The bright value
     is a *fill* - a strip cell, a meter segment, a spine. The -ink value is
     the same colour darkened until it passes AA as *text*. #10B981 on its
     own tint is 2.1:1, which is a badge nobody with low vision can read, so
     badges and figures use the ink and only graphics use the fill. */
  --navy-900: #0B1120;

  /* The sidebar's own palette. Charcoal rather than navy so the emerald
     active state reads as the only colour in the panel - on navy the two
     blues competed and the active item stopped being obvious. */
  /* The rail follows the theme.
  
     It used to be charcoal in both themes - a dark column beside a light
     page, with light text on it. That reads as a different product bolted
     to the side of this one, and in light mode it was the only dark thing
     on the screen.
     
     Light values here, dark ones under [data-theme="dark"] below. The
     emerald identity carries across both: a forest wash at the brand end
     and an emerald rail on the active item, tinted for the surface each
     theme actually has. */
  --sidebar-bg: #FFFFFF;
  --sidebar-forest: #ECFDF5;
  --sidebar-text: #0F172A;
  --sidebar-muted: #64748B;
  --sidebar-active-bg: #ECFDF5;
  --sidebar-active-rail: #059669;
  --sidebar-active-icon: #047857;
  /* Hover needs its own value per theme: lightening works on a dark
     rail and is invisible on a white one. */
  --sidebar-hover: rgba(15, 23, 42, .05);
  --sidebar-edge: #E2E8F0;
  --navy-800: #111C33;
  --navy-700: #17253F;
  --navy-600: #1F304E;

  /* Emerald. The ramp is the only place the brand hue is declared, so the
     whole product follows from these four values - buttons, links, active
     navigation, focus rings, chart series and the platform tint.
     
     The UI stays neutral: emerald is for primary actions and accents, not
     for surfaces. --bg, --surface, --line and the text tokens below are
     unchanged, which is what keeps the change from reading as a repaint. */
  --brand-700: #065F46;   /* hover, pressed, links on white */
  --brand-600: #059669;   /* primary action, active navigation */
  --brand-400: #10B981;   /* on dark, where the 600 goes muddy */
  --brand-050: #ECFDF5;   /* tints behind brand text */

  --accent: #0284C7;      /* info - performance graphics, highlights */
  --accent-ink: #0E7490;
  --accent-050: #ECFEFF;

  --success: #16A34A;     /* win, present, paid */
  --success-ink: #047857;
  --success-050: #ECFDF5;
  --warning: #D97706;     /* draw, pending, partial */
  --warning-ink: #B45309;
  --warning-050: #FFFBEB;
  --danger: #DC2626;      /* loss, absent, overdue */
  --danger-ink: #B91C1C;
  --danger-050: #FEF2F2;
  --slate: #64748B;       /* excused, cancelled, neutral */
  --slate-ink: #475569;
  --slate-050: #F1F5F9;

  /* Surfaces. Slate-tinted rather than neutral grey, so white cards lift off
     the page without a shadow doing all the work. */
  --bg: #F8FAFC;
  --surface: #FFFFFF;
  --surface-2: #F8FAFC;
  --surface-3: #F1F5F9;
  --line: #E2E8F0;
  --line-strong: #CBD5E1;

  --text: #0F172A;
  --text-2: #475569;
  /* The faint tier still has to be readable. #94A3B8 is the natural slate
     step here and lands at 2.6:1 on white - fine for a hairline, not for
     the timestamps and hints that actually use it. */
  --text-3: #64748B;
  --on-navy: #E5E7EB;
  --on-navy-2: #94A3B8;
  /* Section headings in the sidebar. Was rgba(255,255,255,.38), which
     composites to 3.53:1 on the navy - below the 4.5:1 body text needs, and
     they are set in small caps at .82rem, the size that can least afford
     it. */
  --on-navy-3: #8A97A8;
  /* What sits on top of a filled brand button. Light mode paints white on
     a deep blue; dark mode inverts the button to a pale blue, where white
     drops to about 2:1 - so the ink goes dark instead of the fill going
     darker, which would lose the button against the surface. */
  --on-brand: #FFFFFF;

  --radius: 12px;
  --radius-sm: 8px;
  --radius-xs: 5px;

  --shadow-1: 0 1px 2px rgba(16, 36, 58, .06), 0 1px 3px rgba(16, 36, 58, .04);
  --shadow-2: 0 4px 12px rgba(16, 36, 58, .08), 0 1px 3px rgba(16, 36, 58, .05);
  --shadow-3: 0 18px 45px rgba(12, 26, 43, .22);

  --sidebar-w: 236px;   /* was 258 - the labels never came close to filling it */
  /* The collapsed rail. 72px is the smallest width that still holds a 17px
     icon on a 40px hit target with even margins - narrower and the icons
     stop being comfortably tappable, which is the whole point of a rail on
     a laptop with a trackpad. */
  --sidebar-rail: 72px;
  --topbar-h: 60px;

  /* One rhythm for the gaps between sections of a page. Named because it is
     used in a dozen places and the whole value of a rhythm is that it is the
     same number every time. */
  --gap-section: 24px;

  --font-ui: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue",
             Arial, sans-serif;
  --font-data: ui-monospace, "SF Mono", "Cascadia Mono", "Roboto Mono",
               Menlo, Consolas, monospace;

  --ease: cubic-bezier(.22, .61, .36, 1);
}

/* Dark mode. Same hues, inverted surfaces - the outcome colours have to stay
   recognisable, so they lighten rather than change. */
[data-theme="dark"] {
  /* True charcoal, not slate.
     
     The previous values (#0B1220 / #111827 / #172033 / #263247) carried a
     +21 to +33 blue cast - the blue channel ran that much higher than the
     red. They were navy by construction, which is why the emerald tokens
     landed correctly and the screen still read as the old theme. Changing
     the accent could never fix a surface that was itself blue.
     
     These sit at +4 to +13: neutral enough that emerald is the only hue
     on the screen. */
  --bg: #101214;
  --surface: #20252B;
  --surface-2: #292F36;
  --surface-3: #1E293B;
  --line: #343A40;
  --line-strong: #454C54;

  --text: #F5F7F9;
  /* Lifted from #A3AAB3: on the elevated surface (#292F36) that measured
     4.41:1, just under the 4.5 a table header needs. */
  --text-2: #ABB2BB;
  /* Table headers use this, on the elevated surface (#292F36). At
     #8D949C that was 4.41:1 - just under the 4.5 a 11px uppercase label
     needs, and uppercase at that size is already hard work. */
  --text-3: #99A1A9;

  /* On a dark surface the contrast problem inverts: the bright fill is now
     the readable one and the darkened ink disappears, so ink and fill
     converge rather than the ink going darker still. */
  --success: #34D399; --success-ink: #6EE7B7; --success-050: #06281E;
  --warning: #FBBF24; --warning-ink: #FCD34D; --warning-050: #2C1E05;
  --danger:  #F87171; --danger-ink:  #FCA5A5; --danger-050:  #2C1214;
  --slate:   #94A3B8; --slate-ink:   #CBD5E1; --slate-050:   #1B2434;

  /* Lifted for dark surfaces: #059669 on #111827 is 2.6:1, unreadable as
     link text. The ramp inverts so the lighter emeralds carry the weight. */
  --brand-700: #34D399;
  --brand-600: #10B981;
  --brand-400: #6EE7B7;
  --brand-050: #06281E;

  --accent: #22D3EE;
  --accent-ink: #67E8F9;
  --accent-050: #0A2A33;

  --on-brand: #0B1120;

  --shadow-1: 0 1px 2px rgba(0, 0, 0, .3);
  --shadow-2: 0 4px 14px rgba(0, 0, 0, .35);
  --shadow-3: 0 18px 45px rgba(0, 0, 0, .55);
}

* { box-sizing: border-box; }

/* The root type size, and so the whole app's.
   
   Every font-size in this stylesheet is in rem - 174 of them - and not one
   padding, margin, gap or width is. So this scales the text across every
   shell at once (platform, academy, parents) and moves no spacing at all,
   which is why it is done here rather than by editing 174 declarations and
   hoping they stay in step.
   
   17px against the browser's default 16: about 6% larger. Enough to read
   as a correction, small enough that nothing reflows into two lines. */
html {
  font-size: 17px;
  -webkit-text-size-adjust: 100%;
}

body {
  margin: 0;
  background: var(--bg);
  color: var(--text);
  font-family: var(--font-ui);
  font-size: .92rem;   /* was 14.5px - fixed, so it ignored the root */
  line-height: 1.55;
  -webkit-font-smoothing: antialiased;
}

/* Numbers line up in every table without being asked twice. */
table, .num, .stat-value, .money, .strip { font-variant-numeric: tabular-nums; }

a { color: var(--brand-700); text-decoration: none; }
a:hover { text-decoration: underline; }

h1, h2, h3, h4 { margin: 0; font-weight: 650; letter-spacing: -.014em; }
h1 { font-size: 1.5rem; }
h2 { font-size: 1.05rem; }
h3 { font-size: .95rem; }

:focus-visible {
  outline: 2px solid var(--brand-600);
  outline-offset: 2px;
  border-radius: 4px;
}

.mono { font-family: var(--font-data); font-size: .86em; letter-spacing: -.01em; }
.muted { color: var(--text-2); }
.faint { color: var(--text-3); }
.small { font-size: .82rem; }
.num { text-align: right; }
.nowrap { white-space: nowrap; }
.sr-only {
  position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
  overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0;
}

/* The eyebrow: a small uppercase label. Used for section kickers and table
   group headings, never as decoration. */
.eyebrow {
  font-size: .68rem;
  font-weight: 700;
  letter-spacing: .13em;
  text-transform: uppercase;
  color: var(--text-3);
}

/* ==========================================================================
   Shell
   ========================================================================== */

.shell { display: flex; min-height: 100vh; }

/* Sidebar legibility in light mode.
   
   The reported problem was "sidebar text too light against the light
   background". The sidebar is not light - it is navy-900 in both themes -
   so the mechanism is different: the item colour (#94A3B8) measures 7.34:1
   and passes, but sitting beside pure-white active text it reads as
   washed out, and the section headings genuinely failed at 3.53:1.
   
   These lift the resting and heading colours in light mode only, because
   dark mode was asked to stay as it is. Hierarchy is unchanged: the active
   item keeps its white-on-blue pill, which is a stronger distinction than
   a shade of grey ever was.
   
   Set on :root rather than per element so the tokens stay the single place
   these colours are defined. */
:root:not([data-theme="dark"]) {
  --on-navy-2: #C6D2E0;   /* 11.4:1 on navy - resting nav items, foot text */
  --on-navy-3: #9FB0C4;   /*  6.4:1 on navy - section headings            */
}

/* Icons matched their label's weight at .85; at the brighter colour they
   can carry full strength without shouting. */
:root:not([data-theme="dark"]) .sidebar nav a svg { opacity: 1; }

.sidebar {
  /* One glass surface, not a stack of floating cards.
  
     Making every nav item its own glass panel was considered and rejected:
     a rail of twelve translucent cards is twelve edges and twelve shadows
     competing for the same twenty pixels, and the eye stops reading it as
     a list. The container is the glass; the items sit on it.
     
     Blur and saturation come from --glass-blur, the same token the modals
     and drawers use, so there is one glass in this product rather than two
     that drift apart. The surface is mixed here rather than reusing
     --glass-surface because that token is built from --surface, which is
     white in light mode - the rail is charcoal in both themes and always
     was. */
  width: var(--sidebar-w);
  flex: 0 0 var(--sidebar-w);
  background:
    linear-gradient(180deg,
      color-mix(in srgb, var(--sidebar-forest) 86%, transparent) 0%,
      color-mix(in srgb, var(--sidebar-forest) 40%, var(--sidebar-bg)) 18%,
      color-mix(in srgb, var(--sidebar-bg) 88%, transparent) 46%);
  /* No backdrop-filter here - see the 760px block below.
  
     It was here, and it made the sidebar move while the page scrolled.
     A blurred layer has to be recomposited against whatever is behind it
     on every frame, and on a sticky element that means every scroll
     frame. On some GPUs under Chromium that shows up as the rail visibly
     jumping or lagging as you scroll - reported three times before the
     cause was found, because it does not happen on every machine.
     
     And on desktop it bought nothing: the rail sits beside the content,
     not over it, so there was only the flat page colour behind it to
     blur. Measured, not assumed. The glass look comes from the
     translucent gradient and the edge light, which cost nothing to
     scroll. */

  /* Green-tinted alpha rather than --glass-border. That token is
     near-white at .62 for a panel floating over a light page; on a
     charcoal rail it draws a bright line down the screen. Same idea, alpha
     chosen for the surface it actually sits on. */
  border-right: 1px solid var(--sidebar-edge);

  /* Depth, not neon: a soft fall to the right and one hairline of light
     along the inside edge. No glow. */
  box-shadow:
    1px 0 0 rgba(245, 247, 249, .04) inset,
    8px 0 24px -12px rgba(0, 0, 0, .55);
  color: var(--sidebar-text);
  display: flex;
  flex-direction: column;
  position: sticky;
  top: 0;
  height: 100vh;
  z-index: 60;
}

.sidebar .brand {
  /* Even top and bottom. The uneven 18/14 compensated for the descender
     space under "AcademyOS"; with the text gone it reads as the logo
     sitting high in its own band.

     Left padding is 20px, not 18: the navigation's icons start at 20
     (10px nav padding + 10px item inset), and the icon column is what the
     eye reads as the sidebar's left edge. At 18 the logo sat two pixels
     proud of every icon beneath it. The right padding stays 18 - nothing
     aligns to it. */
  padding: 16px 18px 16px 20px;
  border-bottom: 1px solid rgba(255, 255, 255, .08);
}
.sidebar .brand img { width: 150px; display: block; }

/* ==========================================================================
   Brandmark: supplied SportSyncs artwork + AcademyOS set beside it
   ==========================================================================
   The two are separate on purpose. The logo is a fixed asset and is never
   restyled; AcademyOS is live text in Montserrat, the closest match to the
   logo's lettering, sized so its cap-height lines up with the wordmark's.
   A hairline rule keeps them from reading as one word. */

@font-face {
  font-family: "Montserrat";
  src: url("../fonts/montserrat-latin-600-normal.woff2") format("woff2");
  font-weight: 600; font-style: normal; font-display: swap;
}
@font-face {
  font-family: "Montserrat";
  src: url("../fonts/montserrat-latin-700-normal.woff2") format("woff2");
  font-weight: 700; font-style: normal; font-display: swap;
}

.brandmark {
  display: flex; align-items: center; gap: 11px; text-decoration: none;
  min-width: 0;
}
.brandmark .logo { height: 30px; width: auto; display: block; flex: none; }

/* In the sidebar rail there is no room for a side-by-side lockup, so the
   same two marks stack and the rule lies flat between them. */
.sidebar .brandmark {
  /* One mark now, not a stack. "AcademyOS" and the hairline rule that
     divided it from the logo were removed, so the column layout has a
     single child: gap and flex-start describe a stack that no longer
     exists. A single row, vertically centred, is what this is. */
  flex-direction: row; align-items: center; gap: 0;
  min-height: 30px;
}
/* Larger. At 26px the supplied wordmark sat smaller in the rail than it
   does anywhere else in the product, which made the brand look like a
   label on the navigation rather than the thing the navigation belongs
   to. 34px matches the weight of the section headings below it without
   crowding the brand band, whose padding is unchanged. */
.sidebar .brandmark .logo { height: 34px; }
/* The monogram appears only in the rail; the lockup only outside it. */
.sidebar .brandmark .mark { display: none; height: 34px; width: auto; }
[data-rail="1"] .sidebar .brandmark .mark { display: block; }

/* The sign-in lockup: same construction, larger, with room to breathe. */
.brandmark.lockup { gap: 16px; position: relative; }
.brandmark.lockup .logo { height: 46px; }
.brandmark.lockup .rule { height: 34px; }
.brandmark.lockup .os { font-size: 1.62rem; }

.auth .pitch-side footer.built-by {
  display: flex; align-items: center; gap: 10px; flex-wrap: wrap;
}
.auth .pitch-side footer.built-by img {
  width: auto !important; height: 19px; opacity: .88;
  position: relative; top: 1px;
}

/* The same lockup, quieter, at the foot of the sidebar. */
.sidebar .foot .built-by {
  display: flex; align-items: center; gap: 7px;
  margin-top: 12px; padding-top: 11px;
  border-top: 1px solid rgba(255, 255, 255, .08);
  font-size: .68rem; letter-spacing: .07em; text-transform: uppercase;
  color: var(--sidebar-muted);
}
.sidebar .foot .built-by img { height: 15px; width: auto; opacity: .8; }
.sidebar .brand .tenant {
  margin-top: 10px;
  font-size: .8rem;
  color: var(--sidebar-muted);
  display: flex; align-items: center; gap: 7px;
}
.sidebar .brand .tenant .dot {
  width: 6px; height: 6px; border-radius: 50%;
  background: var(--brand-400); flex: none;
}

/* overflow-x: hidden, not just overflow-y: auto.
   
   A box that scrolls on one axis cannot have visible overflow on the
   other - the browser resolves it to auto. So anything positioned beyond
   the rail's right edge inflated this box's scroll width: a horizontal
   scrollbar appeared at the foot of the sidebar, and scrolling it dragged
   the off-rail boxes into view over the page. That was the overlapping
   and the stray "Packa"/"Pricin" fragments. */
.sidebar nav {
  flex: 1;
  overflow-y: auto;
  overflow-x: hidden;
  padding: 10px 10px 20px;
}
.sidebar nav::-webkit-scrollbar { width: 8px; }
.sidebar nav::-webkit-scrollbar-thumb {
  background: rgba(255, 255, 255, .12); border-radius: 8px;
}

.nav-group { margin-top: 14px; }
.nav-group:first-child { margin-top: 4px; }
.nav-group > .eyebrow {
  color: var(--sidebar-muted);
  padding: 0 10px 6px;
  display: block;
}

.sidebar nav a {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 8px 10px;
  border-radius: var(--radius-sm);
  color: var(--sidebar-muted);
  font-size: .875rem;
  font-weight: 500;
  text-decoration: none;
  transition: background .14s var(--ease), color .14s var(--ease);
}
.sidebar nav a svg { width: 17px; height: 17px; flex: none; opacity: .85; }
.sidebar nav a:hover {
  /* Slightly brighter glass, which on a translucent surface means letting
     a little more light through rather than painting a grey box on top. */
  background: var(--sidebar-hover);
  color: var(--sidebar-text);
}
.sidebar nav a.active {
  /* Deeper forest glass, not the brand fill. A solid emerald block the
     width of the item shouted louder than the page it was pointing at;
     the rail carries the signal and the surface just lifts it.
     
     Translucent like everything else on this panel, so the active item
     reads as part of the same material rather than a sticker on it. */
  background: color-mix(in srgb, var(--sidebar-active-bg) 82%, transparent);
  color: var(--sidebar-text);
  /* Edge drawn with an inset shadow, not a border: a 1px border on the
     active item alone would shift its label a pixel against every
     inactive sibling, and a list that twitches as you move down it is
     worse than one with no edge at all. */
  box-shadow:
    inset 3px 0 0 var(--sidebar-active-rail),
    inset 0 0 0 1px rgba(16, 185, 129, .16);
}
.sidebar nav a.active svg { opacity: 1; color: var(--sidebar-active-icon); }
.sidebar nav a .count {
  margin-left: auto;
  background: rgba(255, 255, 255, .14);
  color: #fff;
  border-radius: 20px;
  padding: 1px 7px;
  font-size: .7rem;
  font-weight: 700;
}
.sidebar nav a.active .count { background: rgba(0, 0, 0, .25); }

.sidebar .foot {
  padding: 12px 14px;
  border-top: 1px solid rgba(255, 255, 255, .08);
  font-size: .78rem;
  color: var(--sidebar-muted);
}
/* Not #fff: the rail is white in light mode, and the name vanished. */
.sidebar .foot strong { color: var(--sidebar-text); display: block; font-size: .85rem; }

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

.topbar {
  height: var(--topbar-h);
  /* Glass, from the same tokens the modals, drawers and sidebar use, so
     there is one glass in this product rather than several that drift.
     
     It is sticky and the page scrolls under it, which is the one place
     glass actually earns its keep - an opaque bar hides what passes
     beneath, a translucent one shows that there is more above. */
  background: var(--glass-surface);
  -webkit-backdrop-filter: var(--glass-blur);
  backdrop-filter: var(--glass-blur);
  border-bottom: 1px solid var(--glass-border);
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 0 20px;
  position: sticky;
  top: 0;
  z-index: 50;
}
.topbar .crumb { font-weight: 600; font-size: .93rem; }
.topbar .crumb span { color: var(--text-3); font-weight: 500; }
.topbar .spacer { flex: 1; }

.icon-btn {
  width: 34px; height: 34px;
  display: inline-flex; align-items: center; justify-content: center;
  border: 1px solid var(--line);
  background: var(--surface);
  color: var(--text-2);
  border-radius: var(--radius-sm);
  cursor: pointer;
  position: relative;
  transition: background .14s var(--ease), border-color .14s var(--ease);
}
.icon-btn:hover { background: var(--surface-3); color: var(--text); }
.icon-btn svg { width: 17px; height: 17px; }
.icon-btn .pip {
  position: absolute; top: -4px; right: -4px;
  min-width: 16px; height: 16px; padding: 0 4px;
  background: var(--danger); color: #fff;
  border-radius: 10px;
  font-size: .65rem; font-weight: 700; line-height: 16px;
  border: 2px solid var(--surface);
}

.menu-toggle { display: none; }

.content {
  padding: 22px 20px 56px;
  max-width: 1440px;
  width: 100%;
  margin: 0 auto;
  flex: 1;
}

/* ==========================================================================
   Page header
   ========================================================================== */

.page-head {
  display: flex;
  align-items: flex-end;
  gap: 16px;
  flex-wrap: wrap;
  margin-bottom: var(--gap-section);
}
.page-head .titles { min-width: 0; }
.page-head p { margin: 3px 0 0; color: var(--text-2); font-size: .9rem; }
.page-head .actions { margin-left: auto; display: flex; gap: 8px; flex-wrap: wrap; }

/* ==========================================================================
   Buttons
   ========================================================================== */

.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 7px;
  padding: 8px 14px;
  border-radius: var(--radius-sm);
  border: 1px solid transparent;
  background: var(--brand-700);
  /* #065F46, not #059669.
  
     The palette names #059669 "Primary", but white text on it measures
     3.77:1 - below the 4.5 a 14px semibold label needs. #065F46 gives
     7.68:1 with the same hue family, so the darker step carries the fill
     and #059669 does the work where contrast with white is not required:
     the sidebar active rail, focus rings, borders and chart accents.
     
     Measured, not assumed. Swapping these two was tried and reverted. */
  color: var(--on-brand);
  font: inherit;
  font-size: .875rem;
  font-weight: 600;
  cursor: pointer;
  text-decoration: none;
  white-space: nowrap;
  transition: background .14s var(--ease), transform .08s var(--ease),
              box-shadow .14s var(--ease);
}
.btn:hover { background: #047857; text-decoration: none; }
.btn:active { transform: translateY(1px); }
.btn svg { width: 16px; height: 16px; }

.btn.secondary {
  background: var(--surface);
  color: var(--text);
  border-color: var(--line-strong);
}
.btn.secondary:hover { background: var(--surface-3); }

.btn.ghost { background: transparent; color: var(--text-2); }
.btn.ghost:hover { background: var(--surface-3); color: var(--text); }

.btn.danger { background: var(--danger); color: #fff; }
.btn.danger:hover { background: #DC2626; }

.btn.small { padding: 5px 10px; font-size: .8rem; }
.btn.block { width: 100%; }
.btn[disabled] { opacity: .5; pointer-events: none; }

/* ==========================================================================
   Cards
   ========================================================================== */

.card {
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  box-shadow: var(--shadow-1);
  overflow: hidden;
}
.card + .card { margin-top: var(--gap-section); }
.stats + .card, .card + .stats, .grid + .card { margin-top: var(--gap-section); }
.card > header {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 13px 16px;
  border-bottom: 1px solid var(--line);
  background: var(--surface);
}
.card > header .spacer { flex: 1; }
.card > header p { margin: 2px 0 0; font-size: .82rem; color: var(--text-2); }
.card .body { padding: 16px; }
.card .body.tight { padding: 0; }

.grid { display: grid; gap: 16px; }
.grid-2 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
.grid-3 { grid-template-columns: repeat(3, minmax(0, 1fr)); }
.grid-4 { grid-template-columns: repeat(4, minmax(0, 1fr)); }
.grid.wide-left { grid-template-columns: minmax(0, 1.7fr) minmax(0, 1fr); }
.grid.wide-right { grid-template-columns: minmax(0, 1fr) minmax(0, 1.7fr); }

/* ==========================================================================
   Stat tiles
   ========================================================================== */

.stats { display: grid; gap: 16px; margin-bottom: var(--gap-section); }
.stats.four { grid-template-columns: repeat(4, minmax(0, 1fr)); }
.stats.three { grid-template-columns: repeat(3, minmax(0, 1fr)); }
.stats.two { grid-template-columns: repeat(2, minmax(0, 1fr)); }

.stat {
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  padding: 14px 16px;
  box-shadow: var(--shadow-1);
  position: relative;
  overflow: hidden;
}
/* The spine encodes the tile's state without spending a whole badge on it. */
.stat::before {
  content: "";
  position: absolute; left: 0; top: 0; bottom: 0;
  width: 3px;
  background: var(--brand-600);
}
.stat.good::before { background: var(--success); }
.stat.warn::before { background: var(--warning); }
.stat.bad::before { background: var(--danger); }
.stat.flat::before { background: var(--line-strong); }

.stat .stat-label {
  font-size: .7rem; font-weight: 700; letter-spacing: .11em;
  text-transform: uppercase; color: var(--text-3);
}
.stat .stat-value {
  font-size: 1.85rem;
  font-weight: 680;
  letter-spacing: -.03em;
  line-height: 1.15;
  margin-top: 4px;
}
.stat .stat-value .unit {
  font-size: .95rem; font-weight: 600; color: var(--text-2); margin-left: 2px;
}
.stat .stat-note { font-size: .8rem; color: var(--text-2); margin-top: 3px; }
.stat .stat-note.up { color: var(--success-ink); }
.stat .stat-note.down { color: var(--danger-ink); }
.stat .spark { margin-top: 8px; height: 30px; }

/* ==========================================================================
   The strip - form guide and attendance register
   ========================================================================== */

.strip { display: inline-flex; gap: 3px; align-items: center; }
.strip i {
  width: 17px; height: 17px;
  border-radius: 4px;
  display: inline-flex; align-items: center; justify-content: center;
  font-size: .62rem; font-weight: 800; font-style: normal;
  color: #fff;
  background: var(--slate);
  letter-spacing: 0;
}
.strip i.win, .strip i.present, .strip i.paid { background: var(--success); }
.strip i.draw, .strip i.late, .strip i.partial { background: var(--warning); }
.strip i.loss, .strip i.absent, .strip i.overdue { background: var(--danger); }
.strip i.excused, .strip i.none {
  background: transparent;
  border: 1.5px dashed var(--line-strong);
  color: var(--text-3);
}
.strip.lg i { width: 24px; height: 24px; font-size: .74rem; border-radius: 5px; }

/* ==========================================================================
   Badges
   ========================================================================== */

.badge {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  padding: 2px 8px;
  border-radius: 20px;
  font-size: .74rem;
  font-weight: 650;
  background: var(--slate-050);
  color: var(--slate-ink);
  white-space: nowrap;
}
.badge.brand { background: var(--brand-050); color: var(--brand-700); }
.badge.good { background: var(--success-050); color: var(--success-ink); }
.badge.warn { background: var(--warning-050); color: var(--warning-ink); }
.badge.bad { background: var(--danger-050); color: var(--danger-ink); }
.badge.solid { background: var(--navy-800); color: #fff; }

.pill-row { display: flex; gap: 6px; flex-wrap: wrap; }

/* ==========================================================================
   Tables
   ========================================================================== */

.table-wrap { overflow-x: auto; }

table { width: 100%; border-collapse: collapse; font-size: .875rem; }

thead th {
  text-align: left;
  font-size: .7rem;
  font-weight: 700;
  letter-spacing: .09em;
  text-transform: uppercase;
  color: var(--text-3);
  padding: 9px 14px;
  background: var(--surface-2);
  border-bottom: 1px solid var(--line);
  position: sticky;
  top: 0;
  z-index: 2;
  white-space: nowrap;
}
thead th.num { text-align: right; }

tbody td {
  padding: 10px 14px;
  border-bottom: 1px solid var(--line);
  vertical-align: middle;
}
tbody tr:last-child td { border-bottom: 0; }
tbody tr { transition: background .1s var(--ease); }
tbody tr:hover { background: var(--surface-2); }

/* Row spine: same device as the stat tile, applied to a table row. */
tbody tr[data-state] td:first-child { position: relative; }
tbody tr[data-state] td:first-child::before {
  content: "";
  position: absolute; left: 0; top: 0; bottom: 0; width: 3px;
  background: var(--line-strong);
}
tbody tr[data-state="good"] td:first-child::before { background: var(--success); }
tbody tr[data-state="warn"] td:first-child::before { background: var(--warning); }
tbody tr[data-state="bad"] td:first-child::before { background: var(--danger); }

td .stack { display: flex; flex-direction: column; line-height: 1.3; }
td .stack .sub { font-size: .78rem; color: var(--text-3); }

.row-actions { display: flex; gap: 6px; justify-content: flex-end; }

/* Sort affordance on filterable tables */
th[data-sort] { cursor: pointer; user-select: none; }
th[data-sort]:hover { color: var(--text); }
th[data-sort]::after {
  content: "\2195"; opacity: .3; margin-left: 5px; font-size: .9em;
}
th[data-sort].asc::after { content: "\2191"; opacity: .9; }
th[data-sort].desc::after { content: "\2193"; opacity: .9; }

/* ==========================================================================
   Forms
   ========================================================================== */

.field { display: block; margin-bottom: 13px; }
.field > label,
.field-label {
  display: block;
  font-size: .78rem;
  font-weight: 650;
  color: var(--text-2);
  margin-bottom: 5px;
}
.field .hint { font-size: .76rem; color: var(--text-3); margin-top: 4px; }

input[type=text], input[type=password], input[type=email], input[type=number],
input[type=date], input[type=search], input[type=tel], select, textarea {
  width: 100%;
  padding: 8px 11px;
  border: 1px solid var(--line-strong);
  border-radius: var(--radius-sm);
  background: var(--surface);
  color: var(--text);
  font: inherit;
  font-size: .875rem;
  transition: border-color .14s var(--ease), box-shadow .14s var(--ease);
}
input:focus, select:focus, textarea:focus {
  outline: none;
  border-color: var(--brand-600);
  box-shadow: 0 0 0 3px var(--brand-050);
}
textarea { resize: vertical; min-height: 76px; }
select {
  appearance: none;

  /* The chevron needs width, height AND background-size.
     
     It had a viewBox and none of the three. An SVG with no intrinsic
     dimensions has no intrinsic size either, so the browser scaled it to
     the select's own box - a triangle as tall as the control, painted over
     the end of the label. It read as a rendering fault because it was one.
     
     A chevron rather than a filled triangle: the same shape as the other
     disclosure arrows in the product, and it does not turn into a blob at
     small sizes. currentColor would inherit the text colour, but a
     background-image cannot see it, so the stroke is the muted ink both
     themes already use for hints. */
  background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpath d='M3 4.75 6 7.75l3-3' fill='none' stroke='%237C8C97' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: right 11px center;
  background-size: 12px 12px;

  /* Room for the chevron plus a gap, so a long option ends before the
     arrow rather than running under it. */
  padding-right: 34px;
}

/* Chromium keeps a native arrow on multiple selects even with
   appearance:none, which would sit beside ours. */
select[multiple], select[size]:not([size="1"]) {
  background-image: none;
  padding-right: 12px;
}

.check {
  display: flex; align-items: flex-start; gap: 9px;
  font-size: .87rem; cursor: pointer;
}
.check input { margin-top: 3px; accent-color: var(--brand-700); }

.form-grid { display: grid; gap: 0 14px; }
.form-grid.two { grid-template-columns: repeat(2, minmax(0, 1fr)); }
.form-grid.three { grid-template-columns: repeat(3, minmax(0, 1fr)); }
.form-grid .span-2 { grid-column: span 2; }

.form-actions {
  display: flex; gap: 8px; justify-content: flex-end;
  padding-top: 6px; flex-wrap: wrap;
}

/* Filter bar above a table */
.filters {
  display: flex; gap: 8px; align-items: center; flex-wrap: wrap;
  padding: 11px 16px;
  border-bottom: 1px solid var(--line);
  background: var(--surface-2);
}
.filters input, .filters select { width: auto; min-width: 150px; }
.filters .grow { flex: 1; min-width: 190px; }

/* ==========================================================================
   Drawer - create and edit forms slide in rather than pushing the page down
   ========================================================================== */

/* ---- dialogs: one design, every role -----------------------------------

   `.drawer` and `.modal` are now the same component wearing the same
   surface. They were not: the drawer slid an opaque panel in from the right
   and the modal floated a glass card in the middle, so an Academy Admin
   opening "Register player" and a platform administrator opening "Onboard
   an academy" saw two different products.

   Rather than edit thirty-one dialogs across twenty-three templates, the
   drawer adopts the modal's presentation here. Every screen gets the new
   design at once, no markup moves, and nothing can be missed.

   TOKENS, NOT COPIES. Both read the --glass-* variables below. Two sets of
   hard-coded colours is exactly how the Academy interface drifted bluer
   than the platform one - each had picked up whatever --surface happened to
   be in its own context. */

:root {
  /* One glass surface for the whole product. */
  /* Glass, refined.
  
     Three things separate convincing glass from a translucent rectangle:
     enough blur that what shows through is unreadable shape rather than
     legible content, a lit top edge, and a shadow with two distinct falls.
     The old values had 6px blur - enough to soften text behind the panel
     but not enough to stop you reading it, which is the uncanny middle. */
  --glass-surface: color-mix(in srgb, var(--surface) 82%, transparent);
  --glass-border: rgba(255, 255, 255, .62);
  --glass-scrim: rgba(11, 17, 32, .46);
  --glass-blur: blur(20px) saturate(160%);
  --glass-radius: 18px;

  /* Contact shadow, mid shadow, and a hairline of light along the top
     inside edge - that inset line is what makes an edge look like glass
     catching light rather than a border drawn around a box. */
  --glass-shadow:
    0 1px 2px rgba(11, 17, 32, .06),
    0 12px 28px -8px rgba(11, 17, 32, .18),
    0 32px 64px -16px rgba(11, 17, 32, .28),
    inset 0 1px 0 rgba(255, 255, 255, .72);
  --glass-foot: color-mix(in srgb, var(--surface) 64%, transparent);
}
:root[data-theme="dark"] {
  /* The rail as it was: charcoal, forest wash, emerald rail. */
  --sidebar-bg: #171A1F;
  --sidebar-forest: #06281E;
  --sidebar-text: #F8FAFC;
  --sidebar-muted: #94A3B8;
  --sidebar-active-bg: #064E3B;
  --sidebar-active-rail: #10B981;
  --sidebar-active-icon: #34D399;
  --sidebar-hover: rgba(245, 247, 249, .06);
  --sidebar-edge: rgba(16, 185, 129, .10);

  /* Dark glass needs a touch more opacity than light - a translucent dark
     panel over a dark page has almost nothing to separate it from what is
     behind, so the edge does the work. */
  --glass-surface: color-mix(in srgb, var(--surface) 86%, transparent);
  --glass-border: rgba(245, 247, 249, .14);
  --glass-scrim: rgba(3, 7, 18, .68);
  --glass-shadow:
    0 1px 2px rgba(0, 0, 0, .30),
    0 12px 28px -8px rgba(0, 0, 0, .46),
    0 32px 64px -16px rgba(0, 0, 0, .58),
    inset 0 1px 0 rgba(245, 247, 249, .07);
}

.drawer {
  position: fixed;
  inset: 0;
  z-index: 100;
  display: none;
  align-items: center;
  justify-content: center;
  padding: 24px;
}
.drawer[open], .drawer.open { display: flex; }

.drawer .scrim {
  position: absolute;
  inset: 0;
  background: var(--glass-scrim);
  /* On the scrim, never the panel: blurring the panel blurs its own
     contents in some engines, which is a form nobody can read while
     typing into it. */
  -webkit-backdrop-filter: var(--glass-blur);
  backdrop-filter: var(--glass-blur);
  animation: fade .18s var(--ease);
}

.drawer .panel {
  position: relative;
  width: 100%;
  max-width: 560px;
  /* Bounded, so the body scrolls inside rather than the page behind. */
  max-height: min(86vh, 780px);
  border-radius: var(--glass-radius);
  border: 1px solid var(--glass-border);
  background: var(--glass-surface);
  box-shadow: var(--glass-shadow);
  display: flex;
  flex-direction: column;
  overflow: hidden;
  animation: modal-rise .2s cubic-bezier(.2, .8, .3, 1) both;
}

.drawer .panel > header {
  display: flex;
  align-items: flex-start;
  gap: 10px;
  padding: 18px 20px 13px;
  border-bottom: 1px solid var(--line);
  flex: 0 0 auto;
}
.drawer .panel > header .spacer { flex: 1; }

/* THE FOOTER FIX.

   Callers wrap their content in a <form>, so `.panel > footer` - a direct
   child selector - never matched, and neither did the body's flex rule.
   The footer scrolled away with the content and long forms had a Save
   button nobody could find.

   The form itself becomes the flex column, the body scrolls inside it, and
   the footer is pinned wherever it sits. */
.drawer .panel > form,
.modal .modal-body > form {
  display: flex;
  flex-direction: column;
  min-height: 0;
  flex: 1 1 auto;
}

/* A modal whose caller supplied its own <footer> inside the body.
   
   `.modal-body` is the scroll container, so a footer inside it scrolled -
   and on a phone, where the panel is a bottom sheet with no room to spare,
   it slid past the bottom edge of the glass entirely. The buttons rendered
   outside the panel they belong to.
   
   When a form brings its own footer, the body stops scrolling and hands
   that job to the form's `.body`, exactly as the drawer does. Modals whose
   footer comes from `modal_actions()` are unaffected: it sits outside
   `.modal-body` already. */
.modal .modal-body:has(> form > footer) {
  display: flex;
  flex-direction: column;
  overflow: hidden;
  padding: 0;
  min-height: 0;
}
.modal .modal-body:has(> form > footer) > form > .body {
  padding: 18px 22px;
  overflow-y: auto;
  -webkit-overflow-scrolling: touch;
  flex: 1 1 auto;
  min-height: 0;
}
.modal .modal-body:has(> form > footer) > form > footer {
  flex: 0 0 auto;
  padding: 14px 22px 16px;
  border-top: 1px solid var(--line);
  background: var(--glass-foot);
  display: flex;
  gap: 10px;
  justify-content: flex-end;
}
@media (max-width: 560px) {
  .modal .modal-body:has(> form > footer) > form > .body {
    padding: 14px 16px;
  }
  .modal .modal-body:has(> form > footer) > form > footer {
    padding: 12px 16px calc(14px + env(safe-area-inset-bottom, 0px));
  }
  .modal .modal-body:has(> form > footer) > form > footer .btn {
    flex: 1;
    min-height: 46px;
  }
}
.drawer .panel .body,
.drawer .panel > form > .body {
  padding: 18px 20px;
  overflow-y: auto;
  -webkit-overflow-scrolling: touch;
  flex: 1 1 auto;
  min-height: 0;
}
.drawer .panel footer,
.drawer .panel > form > footer {
  flex: 0 0 auto;
  padding: 14px 20px 16px;
  border-top: 1px solid var(--line);
  background: var(--glass-foot);
  display: flex;
  gap: 10px;
  justify-content: flex-end;
}

/* The modal reads the same tokens rather than its own copies. */
.modal-scrim {
  background: var(--glass-scrim);
  -webkit-backdrop-filter: var(--glass-blur);
  backdrop-filter: var(--glass-blur);
}
.modal-panel {
  border-color: var(--glass-border);
  background: var(--glass-surface);
  box-shadow: var(--glass-shadow);
  border-radius: var(--glass-radius);
}
.modal-foot { background: var(--glass-foot); }

/* Phones: a bottom sheet, for both kinds. A centred card with margins on a
   360px screen wastes the only space there is. */
@media (max-width: 560px) {
  .drawer { padding: 0; align-items: flex-end; }
  .drawer .panel {
    max-width: none;
    max-height: 92vh;
    border-radius: var(--glass-radius) var(--glass-radius) 0 0;
    border-bottom: none;
    animation-name: modal-sheet;
  }
  .drawer .panel > header { padding: 16px 16px 12px; }
  .drawer .panel .body { padding: 14px 16px; }
  .drawer .panel footer {
    padding: 12px 16px calc(14px + env(safe-area-inset-bottom, 0px));
  }
  .drawer .panel footer .btn { flex: 1; min-height: 46px; }
}

@keyframes slide-in { from { transform: translateX(24px); opacity: 0; } }
@keyframes fade { from { opacity: 0; } }

/* ==========================================================================
   Flash messages / toasts
   ========================================================================== */

.toasts {
  position: fixed; right: 18px; bottom: 18px; z-index: 200;
  display: flex; flex-direction: column; gap: 9px;
  max-width: min(400px, calc(100vw - 36px));
}
.toast {
  display: flex; align-items: flex-start; gap: 10px;
  background: var(--surface);
  border: 1px solid var(--line);
  border-left: 3px solid var(--brand-600);
  border-radius: var(--radius-sm);
  box-shadow: var(--shadow-2);
  padding: 11px 13px;
  font-size: .87rem;
  animation: toast-in .24s var(--ease);
}
.toast.success { border-left-color: var(--success); }
.toast.error { border-left-color: var(--danger); }
.toast.warning { border-left-color: var(--warning); }
.toast button {
  margin-left: auto; background: none; border: 0; cursor: pointer;
  color: var(--text-3); font-size: 1.05rem; line-height: 1; padding: 0 2px;
}
@keyframes toast-in { from { transform: translateY(10px); opacity: 0; } }

/* ==========================================================================
   Empty states - an invitation to act, not an apology
   ========================================================================== */

.empty {
  padding: 34px 20px;
  text-align: center;
  color: var(--text-2);
}
.empty .mark {
  width: 40px; height: 40px; margin: 0 auto 10px;
  display: grid; place-items: center;
  border-radius: 11px;
  background: var(--surface-3);
  color: var(--text-3);
}
.empty .mark svg { width: 20px; height: 20px; }
.empty strong { display: block; color: var(--text); font-size: .95rem; }
.empty p { margin: 4px 0 12px; font-size: .86rem; }

/* ==========================================================================
   Charts
   ========================================================================== */

.chart { width: 100%; display: block; }
.chart .grid-line { stroke: var(--line); stroke-width: 1; }
.chart .axis-label { fill: var(--text-3); font-size: 10px; }
.chart .bar { fill: var(--brand-600); }
.chart .bar.alt { fill: var(--navy-600); }
.chart .line { fill: none; stroke: var(--brand-600); stroke-width: 2; }
.chart .area { fill: var(--brand-050); }
.chart .dot { fill: var(--brand-700); }

.legend { display: flex; gap: 14px; flex-wrap: wrap; font-size: .8rem; }
.legend span { display: inline-flex; align-items: center; gap: 6px; }
.legend i { width: 10px; height: 10px; border-radius: 3px; }

/* A labelled proportion bar - used for attendance and collection rates */
.meter { display: flex; flex-direction: column; gap: 5px; }
.meter .track {
  height: 7px; border-radius: 4px; background: var(--surface-3);
  overflow: hidden; display: flex;
}
.meter .track i { display: block; height: 100%; }
.meter .track i.good { background: var(--success); }
.meter .track i.warn { background: var(--warning); }
.meter .track i.bad { background: var(--danger); }
.meter .track i.brand { background: var(--brand-600); }
.meter .track i.flat { background: var(--line-strong); }
.meter .labels {
  display: flex; justify-content: space-between;
  font-size: .78rem; color: var(--text-2);
}

/* ==========================================================================
   Detail pages
   ========================================================================== */

.detail-head {
  display: flex; gap: 16px; align-items: center; flex-wrap: wrap;
  padding: 18px;
}
.avatar {
  width: 54px; height: 54px; border-radius: 14px;
  background: var(--navy-800); color: #fff;
  display: grid; place-items: center;
  font-weight: 700; font-size: 1.15rem; letter-spacing: -.02em;
  flex: none;
}
.avatar.lg { width: 66px; height: 66px; font-size: 1.4rem; }

.facts { display: grid; gap: 1px; background: var(--line); }
.facts.two { grid-template-columns: repeat(2, minmax(0, 1fr)); }
.facts.three { grid-template-columns: repeat(3, minmax(0, 1fr)); }
.facts .fact { background: var(--surface); padding: 11px 16px; }
.facts .fact .k {
  font-size: .68rem; font-weight: 700; letter-spacing: .1em;
  text-transform: uppercase; color: var(--text-3);
}
.facts .fact .v { font-size: .92rem; margin-top: 2px; }

/* Tabs */
.tabs {
  display: flex; gap: 2px; border-bottom: 1px solid var(--line);
  padding: 0 16px; overflow-x: auto;
}
.tabs a, .tabs button {
  padding: 10px 12px;
  font-size: .87rem; font-weight: 600;
  color: var(--text-2);
  border: 0; background: none; cursor: pointer;
  border-bottom: 2px solid transparent;
  white-space: nowrap;
}
.tabs a:hover, .tabs button:hover { color: var(--text); text-decoration: none; }
.tabs a.active, .tabs button.active {
  color: var(--brand-700); border-bottom-color: var(--brand-700);
}

/* ==========================================================================
   Sign in
   ========================================================================== */

.auth {
  min-height: 100vh;
  display: grid;
  grid-template-columns: 1.05fr .95fr;
  background: var(--bg);
}
.auth .pitch-side {
  background:
    radial-gradient(120% 90% at 12% 8%, rgba(69, 167, 157, .28), transparent 58%),
    var(--navy-900);
  color: #fff;
  padding: 46px 52px;
  display: flex; flex-direction: column; justify-content: space-between;
  position: relative;
  overflow: hidden;
}
/* Pitch markings, drawn once, very low contrast. The one piece of ornament
   in the product, and it is literally the subject. */
.auth .pitch-side::after {
  content: "";
  position: absolute; right: -110px; bottom: -140px;
  width: 470px; height: 470px;
  border: 2px solid rgba(255, 255, 255, .07);
  border-radius: 50%;
}
.auth .pitch-side::before {
  content: "";
  position: absolute; right: -260px; top: 50%;
  width: 420px; height: 300px;
  border: 2px solid rgba(255, 255, 255, .06);
  transform: translateY(-50%);
}
.auth .pitch-side img { width: 176px; position: relative; z-index: 1; }
.auth .pitch-side .lede { position: relative; z-index: 1; max-width: 430px; }
.auth .pitch-side .lede h2 {
  font-size: 1.85rem; line-height: 1.22; letter-spacing: -.025em;
  margin-bottom: 10px;
}
.auth .pitch-side .lede p { color: rgba(255, 255, 255, .72); font-size: .95rem; }
.auth .pitch-side .roles {
  position: relative; z-index: 1;
  display: grid; gap: 10px; margin-top: 26px;
}
.auth .pitch-side .roles div {
  display: flex; gap: 10px; align-items: baseline;
  font-size: .85rem; color: rgba(255, 255, 255, .66);
}
.auth .pitch-side .roles strong {
  color: #fff; font-weight: 650; min-width: 118px; display: inline-block;
}
.auth .pitch-side footer { font-size: .78rem; color: rgba(255,255,255,.45); position: relative; z-index: 1; }

.auth .form-side {
  display: flex; align-items: center; justify-content: center; padding: 40px 24px;

}
.auth .form-side .panel { width: 100%; max-width: 372px; }
.auth .form-side h1 { font-size: 1.4rem; margin-bottom: 4px; }
.auth .form-side .sub { color: var(--text-2); font-size: .9rem; margin-bottom: 22px; }
.auth .demo-note {
  margin-top: 20px; padding: 12px 14px;
  background: var(--surface); border: 1px solid var(--line);
  border-radius: var(--radius-sm); font-size: .8rem; color: var(--text-2);
}
.auth .demo-note table { font-size: .78rem; margin-top: 6px; }
.auth .demo-note td { padding: 2px 8px 2px 0; border: 0; }

/* ==========================================================================
   Motion, print and responsive
   ========================================================================== */

/* `backwards`, not `both`. The animation ends on the element's natural state
   - full opacity, no offset - so a forwards fill adds nothing visible and
   costs something real: it leaves `transform` set for good, and an element
   with a transform is a stacking context. Every risen table row became one,
   painted in document order, so a menu opened on one row was covered by
   every row below it. */
.rise { animation: rise .3s var(--ease) backwards; }
@keyframes rise { from { opacity: 0; transform: translateY(6px); } }

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: .001ms !important;
    transition-duration: .001ms !important;
  }
}

@media (max-width: 1080px) {
  .grid-4, .stats.four { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .grid-3, .stats.three { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .grid.wide-left, .grid.wide-right { grid-template-columns: minmax(0, 1fr); }
  .auth { grid-template-columns: minmax(0, 1fr); }
  .auth .pitch-side { display: none; }
}

@media (max-width: 760px) {
  .menu-toggle { display: inline-flex; }
  .sidebar {
    position: fixed; left: 0; top: 0; bottom: 0;
    transform: translateX(-100%);
    transition: transform .24s var(--ease);
    box-shadow: var(--shadow-3);
    /* Here the rail is a drawer sliding over the page, so there is real
       content behind it and the blur is doing actual work. It is also
       only open briefly and never scrolls with the page, which is what
       made it costly on desktop. */
    -webkit-backdrop-filter: var(--glass-blur);
    backdrop-filter: var(--glass-blur);
  }
  body.nav-open .sidebar { transform: none; }
  body.nav-open::after {
    content: ""; position: fixed; inset: 0; z-index: 55;
    background: rgba(9, 20, 32, .45);
  }
  .content { padding: 16px 14px 48px; }
  .grid-2, .grid-3, .grid-4,
  .stats.two, .stats.three, .stats.four { grid-template-columns: minmax(0, 1fr); }
  .form-grid.two, .form-grid.three { grid-template-columns: minmax(0, 1fr); }
  .form-grid .span-2 { grid-column: span 1; }
  .facts.two, .facts.three { grid-template-columns: minmax(0, 1fr); }
  .page-head .actions { width: 100%; }
  .drawer .panel { width: 100%; }

  /* Tables reflow into cards. Each cell carries its own heading so the data
     stays readable without a horizontal scroll. */
  table.responsive thead { display: none; }
  table.responsive tbody tr {
    display: block;
    border: 1px solid var(--line);
    border-radius: var(--radius-sm);
    margin-bottom: 9px;
    padding: 4px 0;
  }
  table.responsive tbody td {
    display: flex; justify-content: space-between; gap: 14px;
    border: 0; padding: 6px 13px; text-align: right;
  }
  table.responsive tbody td::before {
    content: attr(data-label);
    font-size: .7rem; font-weight: 700; letter-spacing: .08em;
    text-transform: uppercase; color: var(--text-3);
    text-align: left; flex: none;
  }
  table.responsive tbody td .stack { align-items: flex-end; }
  table.responsive tbody tr[data-state] td:first-child::before { width: auto; }
}

@media print {
  .sidebar, .topbar, .page-head .actions, .filters, .row-actions,
  .toasts, .btn { display: none !important; }
  body { background: #fff; }
  .card { box-shadow: none; border-color: #ccc; break-inside: avoid; }
  .content { padding: 0; max-width: none; }
}


/* ==========================================================================
   Gallery tiles, branding previews and the public academy pages
   ========================================================================== */

.tiles {
  display: grid; gap: 14px;
  grid-template-columns: repeat(auto-fill, minmax(190px, 1fr));
}
.tiles .tile { margin: 0; }
.tiles .tile img {
  width: 100%; aspect-ratio: 4 / 3; object-fit: cover;
  border-radius: var(--radius); border: 1px solid var(--line);
  background: var(--surface-2); display: block;
}
.tiles .tile figcaption {
  padding: 8px 2px 0; display: flex; flex-direction: column; gap: 4px;
  font-size: .82rem;
}
.tiles .tile figcaption .row-actions { flex-wrap: wrap; gap: 4px; }

img.full {
  width: 100%; height: auto; border-radius: var(--radius);
  border: 1px solid var(--line); display: block;
}
img.brand-preview {
  max-width: 190px; max-height: 130px; padding: 12px; background: var(--surface-2);
  border: 1px solid var(--line); border-radius: var(--radius); display: block;
  margin-bottom: 14px; object-fit: contain;
}
.empty.tight { padding: 18px; margin-bottom: 14px; }

input[type="color"] {
  width: 100%; height: 42px; padding: 4px; cursor: pointer;
  background: var(--surface); border: 1px solid var(--line);
  border-radius: var(--radius-sm);
}

/* -- the public academy page ---------------------------------------------- */

.public { background: var(--surface-2); min-height: 100vh; }
.public .inner { max-width: 960px; margin: 0 auto; padding: 0 20px; }
.public main.inner { padding-bottom: 60px; }
.public .hero {
  background-size: cover; background-position: center; color: #fff;
  padding: 64px 0 44px; margin-bottom: 26px;
}
.public .hero.slim { padding: 34px 0 26px; }
.public .hero h1 { margin: 0 0 6px; font-size: 2rem; letter-spacing: -.02em; }
.public .hero .tagline { margin: 0 0 6px; opacity: .92; font-size: 1.05rem; }
.public .hero .small { opacity: .8; }
.public .hero .logo {
  width: 74px; height: 74px; object-fit: contain; background: #fff;
  border-radius: var(--radius); padding: 8px; margin-bottom: 14px; display: block;
}
.public section { margin-bottom: 22px; }
.public h2 { font-size: 1.05rem; margin: 0 0 12px; }
.public ul.plain { list-style: none; margin: 0; padding: 0; }
.public ul.plain li {
  padding: 9px 0; border-bottom: 1px solid var(--line);
  display: flex; justify-content: space-between; gap: 12px; align-items: baseline;
}
.public ul.plain li:last-child { border-bottom: 0; }
.public .filters { margin-bottom: 18px; flex-wrap: wrap; }
.public .btn.active { background: var(--brand); color: #fff; }
.auth.wide { max-width: 720px; }

/* A single-pane auth screen: card verification has no marketing side. */
.auth.solo {
  display: flex; align-items: center; justify-content: center;
  min-height: 100vh; padding: 24px; background: var(--surface-2);
}
.auth.solo .card { max-width: 460px; width: 100%; }
.auth.solo .facts { display: block; }


/* ==========================================================================
   Sign-in panel motion

   Restrained on purpose: this is the first screen of an operations tool, not
   a landing page. The pitch markings drift slowly enough to read as depth
   rather than movement, and the copy arrives once, staggered, then stops.
   Everything here is disabled outright under prefers-reduced-motion.
   ========================================================================== */

@keyframes ss-rise {
  from { opacity: 0; transform: translateY(14px); }
  to   { opacity: 1; transform: none; }
}
@keyframes ss-drift {
  0%   { transform: translate3d(0, 0, 0) rotate(0deg); }
  50%  { transform: translate3d(-22px, -18px, 0) rotate(4deg); }
  100% { transform: translate3d(0, 0, 0) rotate(0deg); }
}
@keyframes ss-drift-y {
  0%   { transform: translateY(-50%) translateX(0); }
  50%  { transform: translateY(-54%) translateX(-26px); }
  100% { transform: translateY(-50%) translateX(0); }
}
@keyframes ss-glow {
  0%   { opacity: .55; transform: scale(1); }
  50%  { opacity: .95; transform: scale(1.12); }
  100% { opacity: .55; transform: scale(1); }
}
@keyframes ss-in-left {
  from { opacity: 0; transform: translateX(-16px) scale(.96); }
  to   { opacity: 1; transform: none; }
}
@keyframes ss-in-right {
  from { opacity: 0; transform: translateX(14px); }
  to   { opacity: 1; transform: none; }
}
@keyframes ss-rule {
  from { opacity: 0; transform: scaleY(0); }
  to   { opacity: 1; transform: scaleY(1); }
}

/* A soft light that breathes behind the headline, giving the flat gradient
   somewhere to move. */
.auth .pitch-side .glow {
  position: absolute; z-index: 0; pointer-events: none;
  left: -140px; top: 12%;
  width: 520px; height: 520px; border-radius: 50%;
  background: radial-gradient(circle,
      rgba(69, 167, 157, .30) 0%, rgba(69, 167, 157, .10) 45%, transparent 70%);
  filter: blur(6px);
  animation: ss-glow 11s ease-in-out infinite;
}

/* The centre circle and the penalty box drift on different periods, so they
   never march in step. */
.auth .pitch-side::after { animation: ss-drift 26s ease-in-out infinite; }
.auth .pitch-side::before { animation: ss-drift-y 34s ease-in-out infinite; }

/* The two halves of the brandmark arrive from their own sides and meet at
   the rule, which then draws itself down. It states the relationship the
   lockup is making - two separate marks, joined - instead of decorating it. */
.auth .pitch-side .brandmark.lockup .logo {
  animation: ss-in-left .74s cubic-bezier(.2, .7, .3, 1) .10s both;
}
.auth .pitch-side .brandmark.lockup .rule {
  transform-origin: 50% 50%;
  animation: ss-rule .5s cubic-bezier(.4, 0, .2, 1) .52s both;
}
.auth .pitch-side .brandmark.lockup .os {
  animation: ss-in-right .74s cubic-bezier(.2, .7, .3, 1) .26s both;
}

.auth .pitch-side .lede h2,
.auth .pitch-side .lede p,
.auth .pitch-side .roles div,
.auth .pitch-side footer,
.auth .form-side .panel {
  animation: ss-rise .62s cubic-bezier(.22, .68, .36, 1) both;
}
.auth .pitch-side .lede h2 { animation-delay: .14s; }
.auth .pitch-side .lede p  { animation-delay: .21s; }
.auth .pitch-side .roles div:nth-child(1) { animation-delay: .30s; }
.auth .pitch-side .roles div:nth-child(2) { animation-delay: .36s; }
.auth .pitch-side .roles div:nth-child(3) { animation-delay: .42s; }
.auth .pitch-side .roles div:nth-child(4) { animation-delay: .48s; }
.auth .pitch-side .roles div:nth-child(5) { animation-delay: .54s; }
.auth .pitch-side footer { animation-delay: .62s; }
.auth .form-side .panel { animation-delay: .18s; }

/* Each role line lifts very slightly on hover - enough to feel alive when a
   cursor passes, not enough to distract while reading. */
.auth .pitch-side .roles div {
  transition: transform .2s ease, color .2s ease;
}
.auth .pitch-side .roles div:hover {
  transform: translateX(4px); color: rgba(255, 255, 255, .86);
}

@media (prefers-reduced-motion: reduce) {
  /* The brandmark's parts animate individually, so each has to be named
     here. Missing one leaves it parked at its "from" keyframe - which is
     opacity 0, i.e. invisible for exactly the people who need it visible. */
  .auth .pitch-side .glow,
  .auth .pitch-side::before,
  .auth .pitch-side::after,
  .auth .pitch-side .brandmark.lockup .logo,
  .auth .pitch-side .brandmark.lockup .rule,
  .auth .pitch-side .brandmark.lockup .os,
  .auth .pitch-side .lede h2,
  .auth .pitch-side .lede p,
  .auth .pitch-side .roles div,
  .auth .pitch-side footer,
  .auth .form-side .panel {
    animation: none !important;
    opacity: 1 !important;
    transform: none !important;
  }
  .auth .pitch-side::before { transform: translateY(-50%) !important; }
}

/* ==========================================================================
   Theme switching and page entry

   Both live here rather than beside their components because they are
   whole-document concerns: one suppresses every transition on the page for
   a moment, the other runs once per navigation.
   ========================================================================== */

/* Changing theme repaints hundreds of elements at once. Without this, each
   one runs its own colour transition and the switch arrives as a slow,
   uneven wash. The class is added immediately before the attribute flips
   and removed on the next frame, so ordinary hover transitions are
   untouched - only the switch itself is instant. */
.theme-changing,
.theme-changing *,
.theme-changing *::before,
.theme-changing *::after {
  transition: none !important;
}

/* The toggle carries both icons; the current theme decides which is shown.
   Sun means "you are in light mode", and the label says what pressing it
   will do. */
.theme-toggle .when-light { display: inline-flex; }
.theme-toggle .when-dark  { display: none; }
[data-theme="dark"] .theme-toggle .when-light { display: none; }
[data-theme="dark"] .theme-toggle .when-dark  { display: inline-flex; }

/* The sign-out button sits in a form now; the form must not take up
   layout space of its own in the topbar's flex row. */
.topbar .signout { display: inline-flex; margin: 0; }

/* Content-only page entry. The shell stays put between navigations, so
   animating the whole body would move the sidebar and header for no reason
   and cost a frame on every click.

   `backwards`, with only a `from` keyframe - deliberately the same shape as
   `.rise` above, and not `both` with an explicit `to`. A `to` of
   `translateY(0)` filled forwards leaves a *permanent* transform on
   `.content`, and an element with any transform - identity included -
   becomes the containing block for every `position: fixed` descendant
   inside it. That silently re-anchored fixed elements to this element
   instead of the viewport, which is how the row menus ended up positioned
   hundreds of pixels off once they were made fixed. Ending the animation
   with no transform at all avoids the whole class of problem. */
.content {
  animation: page-enter .18s var(--ease, ease-out) backwards;
}

@keyframes page-enter {
  from { opacity: 0; transform: translateY(3px); }
}

/* A system-level request not to animate is a request about all of it,
   including the entry above and any smooth scrolling. */
@media (prefers-reduced-motion: reduce) {
  *,
  *::before,
  *::after {
    animation-duration: .001ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: .001ms !important;
    scroll-behavior: auto !important;
  }
  .content { animation: none !important; opacity: 1; transform: none; }
}

/* ==========================================================================
   Auth pages: sign in, reset, setup, signed out

   The form leads and the artwork supports it. That ordering is deliberate
   in the markup as well as on screen - the panel someone came here to use
   is the first thing a screen reader and a narrow viewport reach, and the
   decoration comes after.
   ========================================================================== */

:root {
  --core-line: rgba(6, 78, 59, .42);
  --core-ink: var(--brand-700);
  /* Opaque, not 72% teal. At that alpha it composited to 3.33:1 against
     the light panel - below the 4.5:1 needed for body text, and it is used
     for the card descriptors at 12px, which is exactly the size that
     cannot borrow the large-text allowance. A slate that keeps the teal
     family without becoming the body-text colour. */
  --core-ink-2: #065F46;
  --core-turf-in: rgba(16, 185, 129, .16);
  --core-turf-out: rgba(16, 185, 129, .04);
  --core-rim-lit: rgba(16, 185, 129, .75);
  --core-rim-edge: rgba(16, 185, 129, .10);
  --core-bloom-in: rgba(16, 185, 129, .13);
  --core-bloom-out: rgba(16, 185, 129, 0);
  --core-glass-top: rgba(255, 255, 255, .92);
  --core-glass-bottom: rgba(255, 255, 255, .62);
  --core-glass-edge: rgba(6, 78, 59, .26);
  --core-node: rgba(16, 185, 129, .85);
  --core-glow: rgba(16, 185, 129, .45);
  --core-bloom-mid: rgba(16, 185, 129, .05);
  --core-grid: rgba(6, 78, 59, .26);
  --core-sheen: rgba(255, 255, 255, .55);
  --core-tile: rgba(16, 185, 129, .12);
  --core-pulse: rgba(16, 185, 129, .70);
  --art-bg-1: #F4F8F6;
  --art-bg-2: #ECFDF5;
  --art-ink: var(--text);
  --art-ink-2: #64748B;
  --art-glow: rgba(16, 185, 129, .28);

  /* The stadium scene. Daylit here - a night match on a white panel would
     be a dark rectangle beside a white form. */
  --art-sky-top: #E8F2EC;
  --art-sky-mid: #DCEDE4;
  --art-sky-low: #CFE6DA;
  --art-turf-far: #D2E6DA;
  --art-turf-near: #C2DCCE;
  --art-stripe: rgba(255, 255, 255, .10);
  --art-chalk: rgba(255, 255, 255, .45);
  --art-stand: #DCEAE1;
  --art-stand-line: rgba(6, 78, 59, .07);
  --art-rig: rgba(6, 78, 59, .42);
  --art-beam: #FFFFFF;
  --art-bloom: #FFFFFF;
  --art-haze: #EAF4EE;
}

[data-theme="dark"] {
  --core-line: rgba(16, 185, 129, .40);
  --core-ink: #6FD39C;
  /* Dark theme: 78% over the panel measures comfortably, but an opaque
     value is one fewer thing that shifts when a surface behind it
     changes. */
  --core-ink-2: #93CFAE;
  --core-turf-in: rgba(16, 185, 129, .26);
  --core-turf-out: rgba(6, 40, 30, .10);
  --core-rim-lit: rgba(52, 211, 153, .95);
  --core-rim-edge: rgba(52, 211, 153, .06);
  --core-bloom-in: rgba(16, 185, 129, .16);
  --core-bloom-out: rgba(16, 18, 20, 0);
  --core-glass-top: rgba(32, 37, 43, .88);
  --core-glass-bottom: rgba(23, 26, 31, .62);
  --core-glass-edge: rgba(16, 185, 129, .40);
  --core-node: rgba(52, 211, 153, .95);
  --core-glow: rgba(52, 211, 153, .55);
  --core-bloom-mid: rgba(16, 185, 129, .06);
  --core-grid: rgba(52, 211, 153, .17);
  --core-sheen: rgba(255, 255, 255, .10);
  --core-tile: rgba(52, 211, 153, .14);
  --core-pulse: rgba(52, 211, 153, .85);
  --art-bg-1: #101214;
  --art-bg-2: #06281E;
  --art-ink: #F5F7F9;
  --art-ink-2: rgba(245, 247, 249, .68);
  --art-glow: rgba(16, 185, 129, .26);

  /* Night match. Charcoal sky, forest turf, emerald floodlights - the same
     four colours as the rest of the product, arranged as a scene. */
  --art-sky-top: #101214;
  --art-sky-mid: #0D1A16;
  --art-sky-low: #06281E;
  /* Turf close to the sky, so the pitch reads as a change of surface
     rather than as a green block under the diagram. */
  --art-turf-far: #0B2A21;
  --art-turf-near: #08201A;
  --art-stripe: rgba(16, 185, 129, .035);
  --art-chalk: rgba(245, 247, 249, .12);
  --art-stand: #14171B;
  --art-stand-line: rgba(16, 185, 129, .07);
  --art-rig: rgba(163, 170, 179, .38);
  --art-beam: #34D399;
  --art-bloom: #34D399;
  --art-haze: #0B2A21;
}

.auth.form-first {
  display: grid;
  grid-template-columns: minmax(0, 460px) minmax(0, 1fr);
  min-height: 100vh;
  min-height: 100dvh;
}

.auth.form-first .form-side {
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 40px 32px;
  order: -1;

  /* Something for the glass to act on.
  
     The panel is translucent and blurred, and blur over a flat colour is
     blur over nothing: it renders, it costs a compositor pass, and it
     looks identical to no blur at all. These washes give it gradient to
     work with, in the same forest and emerald as the rest of the product,
     faint enough that the side still reads as a page rather than a second
     hero. */
  position: relative;
  background:
    radial-gradient(90% 70% at 14% 6%,
      color-mix(in srgb, var(--brand-600) 10%, transparent), transparent 62%),
    radial-gradient(72% 62% at 92% 96%,
      color-mix(in srgb, var(--sidebar-forest) 22%, transparent), transparent 58%),
    var(--surface);
}

.auth.form-first .form-side .panel {
  width: 100%;
  max-width: 356px;

  /* The same glass as the modals and drawers, from the same tokens.
  
     This rule used to flatten the panel deliberately - border 0,
     background none, no shadow - which was right when the side behind it
     was a plain surface and a card would have been a box drawn for no
     reason. It has a backdrop now, so the card has something to sit on.
     
     Specificity matters here: .auth .form-side .panel loses to this, so
     styling the panel there did nothing at all on the two pages that
     actually use this layout. */
  padding: 26px 26px;
  border-radius: var(--glass-radius);
  border: 1px solid var(--glass-border);
  background: var(--glass-surface);
  -webkit-backdrop-filter: var(--glass-blur);
  backdrop-filter: var(--glass-blur);
  box-shadow: var(--glass-shadow);
}

@media (max-width: 560px) {
  /* On a phone the panel is most of the screen, so an edge and a shadow
     around it frame nothing. The backdrop stays; the card stops
     pretending to float. */
  .auth.form-first .form-side .panel {
    border: 0;
    background: none;
    box-shadow: none;
    -webkit-backdrop-filter: none;
    backdrop-filter: none;
    padding: 0;
  }
}

.auth.form-first h1 { font-size: 1.55rem; letter-spacing: -.02em; }
.auth.form-first .sub {
  color: var(--text-2); font-size: .92rem; margin: 4px 0 26px;
}
.auth .forgot { margin-top: 14px; }
.auth .newcomer { margin-top: 22px; color: var(--text-3); }

/* ------------------------------------------------------- reveal button */

.with-reveal { position: relative; display: block; }
.with-reveal input { width: 100%; padding-right: 42px; }

.reveal {
  position: absolute;
  right: 4px;
  top: 50%;
  transform: translateY(-50%);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 32px;
  height: 32px;
  padding: 0;
  border: 0;
  border-radius: 7px;
  background: none;
  color: var(--text-3);
  cursor: pointer;
}
.reveal:hover { color: var(--text-2); background: var(--surface-2); }
.reveal svg { width: 17px; height: 17px; }
.reveal.revealed { color: var(--brand-700); }

/* --------------------------------------------------------- sign-in cta */

.btn.signin {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  margin-top: 6px;
}
.btn.signin svg { width: 17px; height: 17px; transition: transform .18s var(--ease, ease-out); }
.btn.signin:hover svg { transform: translateX(3px); }

/* -------------------------------------------------------- the art side

   Three rows: masthead, artwork, footer. A grid rather than a centred
   column, because the footer has to sit in a corner and the artwork has to
   stay optically centred in what is left - which a flex column with
   `justify-content: center` cannot do once one child is pinned. */

.art-side {
  /* Never taller than the screen.
  
     The row below is 1fr with min-height: 0 so it can shrink - but a row
     only shrinks against a definite height, and .auth sets min-height:
     100vh, which is a floor, not a ceiling. So the pane grew to whatever
     the artwork needed, pushed the lede and the footer past the bottom of
     the window, and overflow: hidden below quietly cut them off. It looked
     correct on a tall screen and broken on a short one, which is the worst
     way for a layout bug to behave: nobody who can reproduce it owns the
     machine where it happens.
     
     Capping the pane rather than the whole .auth grid, so the form beside
     it can still be taller than the viewport and scroll - sign-up forms
     legitimately are. */
  max-height: 100vh;
  position: relative;
  /* Clipped, deliberately: the background gradient and the soft glow behind
     it must not bleed over the form beside them. The artwork inside is kept
     within bounds by the row below rather than by letting this overflow -
     see .art-side .core-row. */
  overflow: hidden;
  display: grid;
  grid-template-rows: auto 1fr auto;
  row-gap: 20px;
  padding: 44px 48px 34px;
  background: linear-gradient(150deg, var(--art-bg-1), var(--art-bg-2));
  color: var(--art-ink);
  text-align: center;
}

.art-stadium .pylons,
.art-stadium .stripes,
.art-stadium .markings {
  /* Additive. Painted normally, a 50%-opacity cone over a near-black sky
     is still darker than the light it is meant to be; screen adds to what
     is behind it, which is what light does. */
  mix-blend-mode: screen;
}

.art-stadium {
  /* Atmosphere, and only that.
  
     Held at roughly a quarter of the strength it was drawn at. The command
     core, the six cards and the connecting lines are the subject; if the
     stadium is legible as a stadium it is too loud. What should survive is
     a sense of depth and a little light along the top - not a scene. */
  opacity: .26;
  /* Behind everything, filling the panel. `slice` on the viewBox keeps the
     horizon level at any aspect ratio rather than letterboxing the scene
     when the panel is tall and narrow. */
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  pointer-events: none;
  z-index: 0;
}

.art-glow {
  position: absolute;
  inset: -20%;
  background: radial-gradient(closest-side, var(--art-glow), transparent 72%);
  pointer-events: none;
}

.art-brand { display: flex; flex-direction: column; align-items: flex-start; }

.art-tagline {
  /* The SportSyncs brand signature, not helper text.
  
     Regular weight, near-normal tracking. 600 with open tracking was
     tried and read as a second heading competing with the wordmark above
     it; the line should sit quietly under the mark, which is what a
     signature does. No glow, no shadow, no uppercase.
     
     Sized in px rather than rem so it holds its relationship to the
     supplied artwork above it if the root size ever changes. */
  margin: 8px 0 0 2px;
  font-size: 12.5px;
  font-weight: 400;
  line-height: 1.25;
  letter-spacing: .005em;
  font-style: normal;
  text-transform: none;
  text-shadow: none;
  color: var(--art-ink-2);
  text-align: left;
  white-space: nowrap;
}

.art-side .art-masthead,
.art-side .core-row,
.art-side .art-lede,
.art-side .core,
.art-side .built-by { position: relative; z-index: 1; }

/* --------------------------------------------------------- the masthead */

.art-masthead {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 24px;
  width: 100%;
}
.art-mark { display: block; flex: none; line-height: 0; }
.art-mark img { height: 62px; width: auto; display: block; }

/* Theme-paired artwork. Both files ship; CSS picks one. */
/* Theme-swapped artwork, scoped to nothing in particular on purpose.

   These rules used to be written `.art-side .when-light` and so on. The
   moment the same pair of images was used anywhere else - the sign-in card
   - neither rule matched and *both* logos rendered, one above the other.
   A visibility rule that only works in one container is a rule that fails
   silently the first time somebody reuses the markup it belongs to.
   
   `:not(.theme-toggle *)` keeps the toggle's own icons on their existing
   inline-flex rules below, which need a different display value. */
.when-dark:not(.theme-toggle *) { display: none; }
[data-theme="dark"] .when-light:not(.theme-toggle *) { display: none; }
[data-theme="dark"] .when-dark:not(.theme-toggle *) { display: block; }

.art-name { text-align: right; min-width: 0; }
.art-wordmark {
  display: flex; align-items: center; justify-content: flex-end; gap: 11px;
  font-family: "Montserrat", var(--font-ui), sans-serif;
  line-height: 1;
}
.art-wordmark .os {
  font-weight: 600;
  font-size: 1.5rem;
  letter-spacing: -.005em;
  color: var(--core-ink);
}
.art-slogan {
  margin: 8px 0 0;
  font-size: .92rem;
  color: var(--art-ink-2);
  font-style: italic;
}

/* ----------------------------------------------------------- the artwork */

/* The middle row centres its own contents, so the diagram sits in the
   optical middle of the panel rather than being pushed down by whichever
   of the two text blocks happens to be taller. */
.art-side .core-row {
  display: grid;
  grid-template-rows: minmax(0, 1fr) auto;
  place-items: center;
  align-content: center;
  gap: 18px;
  /* min-height: 0 lets this grid row shrink below its content, which is
     what allows the masthead above and the lede and footer below to stay
     on screen at 768px-tall laptops instead of being pushed off. */
  min-height: 0;
  /* The row is the scroll/clip boundary for the artwork. Without this the
     SVG is clipped by .art-side's own overflow: hidden at the panel edge,
     which cut the outermost cards in half. */
  overflow: visible;
}

/* The SVG scales to the row's width, and its height follows from the
   viewBox. Capping the height is what stops a tall panel scaling the
   drawing up until the cards collide with the masthead: past this point
   the artwork stays put and the panel gets whitespace instead. */
/* The artwork scales to the space, rather than being cropped to it.

   max-height alone capped the wrapper while the SVG inside kept its
   intrinsic size and spilled over the lede and the footer beneath it.
   min-height: 0 lets the wrapper shrink, and giving the SVG both a width
   and a height of 100% lets its own viewBox do the scaling - it keeps its
   aspect ratio and simply gets smaller on a short screen. */
.art-side .core {
  min-height: 0;
  max-height: 100%;
  display: flex;
  justify-content: center;
}
.art-side .core svg {
  max-height: 100%;
  max-width: 100%;
  height: auto;
  width: auto;
}

.art-side .art-lede { max-width: 440px; margin: 0 auto; }
.art-side .art-lede h2 {
  font-family: var(--font-display, inherit);
  font-size: 1.16rem;
  font-weight: 650;
  letter-spacing: -.01em;
  margin-bottom: 5px;
}
.art-side .art-lede p { color: var(--art-ink-2); font-size: .89rem; margin: 0; }

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

/* Bottom right, on its own row, so it never crowds the artwork and never
   moves when the lede above it runs to two lines instead of one. */
.art-side .built-by {
  display: inline-flex; align-items: center; gap: 8px;
  justify-self: end;
  font-size: .76rem; color: var(--art-ink-2); opacity: .8;
}
.art-side .built-by img { height: 16px; }

/* ----------------------------------------------- the academy command core */

/* The hero of every page reachable without signing in: a top-down academy
   with the AcademyOS mark in a glass core and six live cards around it.
   Geometry, the transform rule and the figures policy are documented in
   partials/auth_art.html next to the shapes they describe.

   One section, in drawing order - background, stadium, core, links, cards,
   then motion. Earlier passes declared .core-card, .core-gridfield and
   .core-links .link in two places each, which is how a hover rule ends up
   fighting an entrance rule nobody remembers writing. */

.core-wide { width: min(1040px, 100%); }
.core svg { width: 100%; height: auto; display: block; overflow: visible; }

/* ---- background depth -------------------------------------------------- */

.core-gridfield { opacity: .8; animation: auth-fade 1.2s ease-out .10s both; }

.core-rings circle {
  stroke: var(--core-line);
  fill: none;
  stroke-width: 1.1;
}
.core-rings .ring-a { opacity: .34; stroke-dasharray: 3 12; }
.core-rings .ring-b { opacity: .46; stroke-dasharray: 58 18; }
.core-rings .ring-c { opacity: .30; stroke-dasharray: 2 8; }
.core-rings .ring-d { opacity: .50; }

.core-ticks line { stroke: var(--core-line); stroke-width: 1.6; opacity: .48; }

.core-traces path {
  stroke: var(--core-line);
  stroke-width: 1.3;
  opacity: .58;
  stroke-linecap: round;
  stroke-linejoin: round;
}
.core-traces circle { fill: var(--core-node); opacity: .7; }

.core-dust circle { fill: var(--core-ink); opacity: .42; }

/* ---- the stadium ------------------------------------------------------- */

/* Four arc segments, not a solid ring: stands have gaps, and the gaps are
   what stop this reading as a running track. */
.core-stadium .stadium-stands path {
  stroke: var(--core-ink);
  stroke-width: 3.6;
  stroke-linecap: round;
  opacity: .6;
  filter: drop-shadow(0 0 8px var(--core-glow));
}
.core-stadium .pitch-turf { stroke: none; }
.core-stadium .pitch-line {
  stroke: var(--core-ink);
  stroke-width: 1.6;
  opacity: .7;
}
.core-stadium .pitch-mark {
  stroke: var(--core-ink);
  stroke-width: 1.2;
  opacity: .42;
  fill: none;
}

/* ---- the hexagonal core ------------------------------------------------ */

.core-hex .hex-fill { fill: url(#core-glass); stroke: none; }
.core-hex .hex-edge {
  stroke: var(--core-ink);
  stroke-width: 2.8;
  stroke-linejoin: round;
  filter: drop-shadow(0 0 14px var(--core-glow));
}
/* A second inset outline. One line reads as a wireframe; two read as a
   machined edge, which is the whole difference in feel. */
.core-hex .hex-inner {
  stroke: var(--core-ink);
  stroke-width: 1.2;
  stroke-linejoin: round;
  opacity: .4;
}
.core-hex .hex-halo {
  stroke: var(--core-ink);
  stroke-width: 1.4;
  opacity: .2;
}
.core-hex .hex-node {
  fill: var(--core-node);
  stroke: none;
  filter: drop-shadow(0 0 7px var(--core-glow));
}

/* ---- the mark ---------------------------------------------------------- */

.core-mark .mark-s {
  stroke: var(--core-ink);
  stroke-width: 11;
  stroke-linecap: round;
  filter: drop-shadow(0 0 10px var(--core-glow));
}
.core-mark .mark-name {
  font-family: var(--font-ui);
  font-size: 25px;
  font-weight: 700;
  letter-spacing: .04em;
  fill: var(--core-ink);
  stroke: none;
}

/* ---- the connections --------------------------------------------------- */

.core-links .link {
  stroke: var(--core-line);
  stroke-width: 1.4;
  opacity: .85;
}

/* One short dash travelling each curve. pathLength normalises every path to
   100 units regardless of real length, so six curves of different lengths
   carry a dash of the same apparent size at the same apparent speed -
   without it the short links look faster and fatter than the long ones.

   The dash runs from the card toward the core, which is the direction the
   data actually moves. */
.core-links .pulse {
  stroke: var(--core-pulse);
  stroke-width: 2.4;
  stroke-linecap: round;
  pathLength: 100;
  stroke-dasharray: 8 92;
  opacity: .75;
  animation: core-travel 9s linear infinite;
  animation-delay: calc(var(--i) * 1.5s);
}

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

/* .core-card is the INNER group. Its parent carries the translate that
   places the card, and nothing here may add a positional transform - see
   the note in partials/auth_art.html. */
.core-card {
  transform-box: fill-box;
  transform-origin: center;
  transition: transform .24s ease;
  filter: drop-shadow(0 14px 24px rgba(0, 0, 0, .34));
}

.core-card .card-edge { stroke: var(--core-glass-edge); stroke-width: 1.2; }
.core-card .card-sheen { opacity: .55; }
.core-card .card-accent { fill: var(--core-ink); opacity: .9; }
.core-card .card-tile {
  fill: var(--core-tile);
  stroke: var(--core-glass-edge);
  stroke-width: 1;
}

.core-card .card-glyph {
  stroke: var(--core-ink);
  stroke-width: 1.7;
  stroke-linecap: round;
  stroke-linejoin: round;
}
/* A few glyphs use <rect> and <circle> as outlines, which would otherwise
   fill solid black and swallow the icon. */
.core-card .card-glyph rect,
.core-card .card-glyph circle,
.core-card .card-glyph ellipse,
.core-card .card-glyph polygon { fill: none; }

.core-card text { stroke: none; font-family: var(--font-ui); }
/* 15px in user units so it is still ~12px CSS at the smallest width this
   layout is used at. See the breakpoint note below. */
.core-card .card-label {
  font-size: 15px;
  font-weight: 650;
  letter-spacing: .1em;
  fill: var(--art-ink-2);
}
/* Tabular figures so a card whose number changes does not jitter. */
.core-card .card-value {
  font-size: 36px;
  font-weight: 700;
  letter-spacing: -.01em;
  fill: var(--art-ink);
  font-variant-numeric: tabular-nums;
}
.core-card .card-note { font-size: 13px; fill: var(--core-ink-2); }
.core-card .card-note-only { font-size: 14px; }

.core-card .card-edge { transition: stroke .24s ease; }
.core-card:hover { transform: translateY(-4px); }
.core-card:hover .card-edge { stroke: var(--core-ink); }

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

/* The panel assembles once: background settles, the core lands, the links
   draw outward, the cards arrive in sequence. Under a second, and it never
   repeats. Only three things continue afterwards - the travelling pulses,
   the breathing halo and the outermost ring - and all three are slow enough
   to be felt rather than watched. */

.core-bloom     { animation: auth-fade 1.0s ease-out both; }
.core-rings     { animation: auth-fade .9s ease-out .14s both; }
.core-ticks     { animation: auth-fade .9s ease-out .20s both; }
.core-traces    { animation: auth-fade .9s ease-out .26s both; }
.core-dust      { animation: auth-fade 1.1s ease-out .30s both; }
.core-stadium   { animation: auth-fade .8s ease-out .18s both; }
.core-hex       { animation: auth-fade .7s ease-out .26s both; }

.core-mark {
  transform-box: fill-box;
  transform-origin: center;
  animation: core-pop .5s cubic-bezier(.34,1.4,.5,1) .32s both;
}

.core-hex .hex-halo {
  animation: auth-fade .6s ease-out .4s both,
             core-breathe 7s ease-in-out 1.4s infinite;
}
.core-hex .hex-node {
  animation: auth-fade .4s ease-out calc(.5s + var(--i) * .06s) both,
             core-glimmer 5s ease-in-out calc(1.6s + var(--i) * .4s) infinite;
}

/* One revolution every four minutes. Under the threshold where motion pulls
   the eye off the form beside it, over the one where it reads as a still
   image. */
.core-rings .ring-a {
  transform-box: fill-box;
  transform-origin: center;
  animation: core-orbit 240s linear 1s infinite;
}

.core-links .link {
  pathLength: 100;
  stroke-dasharray: 100;
  animation: core-draw .6s cubic-bezier(.22,.72,.3,1)
             calc(.4s + var(--i) * .07s) both;
}

.core-card {
  animation: core-rise .5s cubic-bezier(.22,.8,.3,1)
             calc(.56s + var(--i) * .09s) both;
}

@keyframes core-travel { from { stroke-dashoffset: 100; } to { stroke-dashoffset: 0; } }
@keyframes core-draw   { from { stroke-dashoffset: 100; } to { stroke-dashoffset: 0; } }
@keyframes core-pop    { from { opacity: 0; transform: scale(.76); } to { opacity: 1; transform: scale(1); } }
@keyframes core-rise   { from { opacity: 0; transform: translateY(14px); } to { opacity: 1; transform: none; } }
@keyframes core-orbit  { from { transform: rotate(0); } to { transform: rotate(360deg); } }
@keyframes core-breathe { 0%, 100% { opacity: .12; } 50% { opacity: .34; } }
@keyframes core-glimmer { 0%, 100% { opacity: .75; } 50% { opacity: 1; } }

/* Motion is decoration, and decoration is the first thing to go when
   somebody has asked the operating system to stop moving things. The
   entrance goes too: they should get the finished panel, not a slower
   version of it assembling. */
@media (prefers-reduced-motion: reduce) {
  .core-bloom, .core-gridfield, .core-rings, .core-rings .ring-a,
  .core-ticks, .core-traces, .core-dust, .core-stadium, .core-hex,
  .core-hex .hex-halo, .core-hex .hex-node, .core-mark,
  .core-links .link, .core-links .pulse, .core-card, .core-lock {
    animation: none;
  }
  .core-links .link { stroke-dashoffset: 0; }
  .core-links .pulse { opacity: .35; }
  .core-card:hover { transform: none; }
}

/* ---- the compact layout ------------------------------------------------ */

/* Type is set in user units against a 460-wide viewBox, so it scales with
   the drawing. These sizes are chosen so the smallest label lands near
   11px CSS on a 360px screen - the floor for something meant to be read
   rather than glanced at. */
/* Sized for the narrowest screen this has to work on. At 360px the column
   is ~336px against a 460 viewBox - a scale of 0.73 - so 15px here lands
   at about 11px CSS, the floor. Smaller looked fine at 460 and was
   unreadable on a phone. */
.core-compact .card-label { font-size: 15px; letter-spacing: .05em; }
.core-compact .card-value { font-size: 26px; }
.core-compact .card-note { font-size: 13px; }
.core-compact .card-note-only { font-size: 13.5px; }
.core-compact .mark-name { font-size: 17px; }
.core-compact .mark-s { stroke-width: 8; }

/* Decoration is cut back rather than scaled down: a grid and four orbital
   rings at this size are noise, and they are the first thing worth
   spending to keep the cards legible. */
.core-compact .core-rings circle { opacity: .3; }
.core-compact .core-card { filter: drop-shadow(0 8px 14px rgba(0, 0, 0, .3)); }

/* No hover on a touch screen, so the lift would only ever fire on a tap
   and stick. */
@media (hover: none) {
  .core-card:hover { transform: none; }
}

/* ---- the secured state ------------------------------------------------- */

/* The signed-out page draws the same system with the power down: subdued
   cards, no travelling data, and a lock ring closed around the core.
   Recognisably the same product, unmistakably a different state. */

.core-secured .core-card { opacity: .55; filter: none; }
.core-secured .core-card:hover { transform: none; }
.core-secured .core-links .link { opacity: .4; }
.core-secured .core-stadium .stadium-stands path { opacity: .3; filter: none; }
.core-secured .core-hex .hex-edge { filter: none; opacity: .75; }
.core-secured .core-hex .hex-node { filter: none; opacity: .6; }
.core-secured .core-dust circle { opacity: .22; }
/* The one thing that does not dim. The lock is the message. */
.core-secured .core-mark .mark-s { opacity: .85; }

.core-lock .lock-ring {
  stroke: var(--core-ink);
  stroke-width: 1.6;
  stroke-dasharray: 6 10;
  opacity: .55;
}
.core-lock .lock-ring-inner {
  stroke: var(--core-ink);
  stroke-width: 1;
  opacity: .25;
}
.core-lock .lock-body { fill: var(--core-tile); stroke: var(--core-ink); stroke-width: 2; }
.core-lock .lock-shackle { stroke: var(--core-ink); stroke-width: 4; stroke-linecap: round; }
.core-lock .lock-pin { fill: var(--core-ink); }

/* The ring turns once on arrival and stops - a latch closing, not a
   spinner. A signed-out page that keeps moving looks like it is still
   doing something, which is the opposite of the reassurance it owes. */
.core-lock {
  transform-box: fill-box;
  transform-origin: center;
  animation: core-latch .9s cubic-bezier(.22,.72,.3,1) .3s both;
}
@keyframes core-latch {
  from { opacity: 0; transform: rotate(-14deg) scale(1.04); }
  to   { opacity: 1; transform: none; }
}

/* ---- responsive -------------------------------------------------------- */

/* The artwork scales as one unit, so these are size changes rather than
   reflows. The form is what someone came for, so the artwork is what gives
   up space. The max-height pair lives with the .core-row rules above, which
   is where the clipping it prevents is explained. */

@media (max-width: 1100px) { .core { width: 100%; } }

/* A short viewport - a 768px-tall laptop at 125% scaling is effectively
   614px - cannot hold the masthead, the artwork, the lede and the footer. */
/* Height guards for the *side-by-side* composition only, where the panel
   is full height and the artwork competes with the masthead and footer for
   it. Above 900px, because below that the panel is stacked and scrolls -
   a short viewport there is a phone in landscape, and the artwork ending
   up hidden on it was never the intent. */
@media (max-height: 760px) and (min-width: 901px) {
  /* Whichever is smaller: 52% of a short screen, or the space the row
     actually has. This was 52vh alone, which overrode the 100% in the
     base rule and let the artwork grow past its row again - so on a
     1280x720 screen, or a 1366x768 one at 125% zoom, the lede landed back
     on top of it. A cap that ignores the container is not a cap. */
  .art-side .core { max-height: min(52vh, 100%); }
}
@media (max-height: 580px) and (min-width: 901px) {
  .art-side .core-row .core { display: none; }
}

/* A narrow art pane.
   
   Two columns still, but the right one is only a few hundred pixels wide -
   which is what 1366x768 at 150% browser zoom gives. The masthead is a row
   with the mark at one end and the name at the other and nowhere to wrap,
   so "AcademyOS", its slogan and the footer ran off the right edge and
   were cut by overflow: hidden. Vertical fitting was fixed first and this
   was still wrong, because nothing was measuring sideways. */
@media (max-width: 1150px) and (min-width: 901px) {
  .art-masthead {
    flex-wrap: wrap;
    justify-content: center;
    gap: 10px;
  }
  .art-side .art-name { text-align: center; }
  .art-side .art-name,
  .art-side .art-slogan { max-width: 100%; }
  .art-side .built-by { justify-content: center; }
}

/* ---- which layout paints ----------------------------------------------- */

/* Two SVGs, one painted. The orbital arrangement needs width a phone does
   not have - six cards around a centre - so below 640px the portrait
   layout takes over rather than the artwork disappearing, which is what it
   used to do. The hidden one is display:none and is never laid out. */
.core-compact { display: none; }

/* The layout switches on whether the wide composition can still be read,
   not on where the panel happens to stack.
   
   Worked out from the geometry rather than guessed. The wide SVG is 1000
   units across and its labels are set in user units, so they shrink with
   the drawing. In a viewport of width W the art column is roughly W - 520
   after the form and its gutters, and the SVG takes ~94% of that. Below
   about 1280px that puts the 15px label under 11px CSS, which is the floor
   for something meant to be read rather than glanced at:
   
       1440px -> scale 0.87 -> label 13.0px
       1366px -> scale 0.80 -> label 11.9px
       1280px -> scale 0.71 -> label 10.7px   <- switches here
       1180px -> scale 0.62 -> label  9.3px   (compact instead)
   
   Between 1280px and 900px the compact drawing sits beside the form, which
   is fine - it is built for a column and 460px is a column. Below 900px
   the panel stacks under the form as well. */
@media (max-width: 1280px) {
  .core-wide { display: none; }
  .core-compact {
    display: block;
    width: 100%;
    /* The compact viewBox is 460 wide. Past that the drawing only gets
       bigger, not better - it is designed for a column, not a canvas. */
    max-width: 460px;
    margin: 0 auto;
  }
  /* The row stops being a hero and becomes a companion to the form. */
  .art-side .core-row { padding-top: 8px; }
}

/* ------------------------------------------------------- signed out page */

.auth.signed-out {
  display: grid;
  /* .auth is a two-column split for the sign-in pages. This page has one
     thing on it, and without resetting the columns the panel is centred
     inside the first column rather than on the page - which reads as a
     layout that has lost its other half. */
  grid-template-columns: minmax(0, 1fr);
  place-items: center;
  min-height: 100vh;
  min-height: 100dvh;
  padding: 40px 24px;
  background:
    radial-gradient(closest-side, var(--art-glow), transparent 70%) center/70% 70% no-repeat,
    var(--bg);
}

.lock-panel { max-width: 400px; text-align: center; }
.lock-panel h1 { font-size: 1.5rem; margin-bottom: 6px; }
.lock-panel .sub { color: var(--text-2); font-size: .93rem; margin-bottom: 24px; }
.lock-panel .closing { margin-top: 18px; color: var(--text-3); }
.lock-panel .btn.block { justify-content: center; }

.lock {
  position: relative;
  display: grid;
  place-items: center;
  width: 132px;
  height: 132px;
  margin: 4px auto 22px;
}
.lock-orbit {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  overflow: visible;
}
.lock-track {
  stroke: var(--core-ink);
  stroke-width: 1;
  stroke-dasharray: 2 7;
  transform-origin: 66px 66px;
}
.lock-ring {
  position: absolute;
  inset: 0;
  border-radius: 50%;
  border: 1px solid var(--core-glass-edge);
}
.lock-ring.two { inset: 16px; opacity: .55; }

.lock-mark { width: 62px; height: 62px; overflow: visible; }
.lock-mark rect,
.lock-mark path,
.lock-mark circle {
  fill: none;
  stroke: var(--core-ink);
  stroke-width: 2.6;
  stroke-linecap: round;
  stroke-linejoin: round;
}
.lock-mark .body { fill: color-mix(in srgb, var(--core-ink) 12%, transparent); }

/* ---------------------------------------------------------- narrow view */

@media (max-width: 900px) {
  /* minmax(0, 1fr), not 1fr.
     
     A `1fr` track has an automatic minimum of min-content, so it refuses to
     shrink below the widest unbreakable thing inside it. At 360px that put
     both columns at 370px inside a 360px container and gave the document
     10px of horizontal scroll - found in the browser, not in the CSS, and
     invisible at every width above it. minmax(0, ...) removes the floor and
     lets the track take the container's width. */
  .auth.form-first { grid-template-columns: minmax(0, 1fr); }

  /* The panel stacks under the form instead of being hidden.
     
     It used to be `display: none` from 900px down, on the reasoning that
     the artwork is decoration and would push the form below the fold. The
     first half was right and the second was solved the wrong way: hiding
     the whole panel also hid the compact Command Core built for exactly
     this width, so the layout that was supposed to take over below 640px
     could never appear at all. Two rules, written months apart, cancelling
     each other out.

     The form keeps the fold - it is `order: -1` and sized to the viewport,
     so it is what loads under the thumb - and the artwork sits beneath it
     for anyone who scrolls. */
  .auth.form-first .art-side {
    display: flex;
    flex-direction: column;
    justify-content: center;
    order: 2;
    /* Not 100dvh: the panel must be able to end. Given the full viewport
       it would occupy a second screen of its own and the lede beneath it
       would never be reached. */
    min-height: 0;
    padding: 28px 20px 36px;
  }
  .auth.form-first .form-side {
    order: -1;
    /* min-height, not height: the card grows past the viewport on a small
       phone once the error summary and both federated buttons are in, and
       a fixed height would clip it. */
    min-height: 100dvh;
  }

  /* The masthead and the footer belong to the desktop composition, where
     they balance a full-height panel. Stacked under a form they are two
     more things between the artwork and the lede. */
  .auth.form-first .art-brand { display: flex; flex-direction: column; align-items: flex-start; }



.art-side .art-masthead,
  .auth.form-first .art-side .built-by { display: none; }

  .auth.form-first .art-side .core-row { gap: 22px; }
}

@media (max-width: 480px) {
  /* 40px of side padding is 11% of a 360px screen, and it comes straight
     off the artwork's width - which is what sets the type size. */
  .auth.form-first .art-side { padding: 20px 12px 28px; }
}

/* ==========================================================================
   Auth motion

   One orchestrated entry rather than scattered effects: the form arrives
   first because that is what the visit is for, then the artwork assembles
   behind it - centre outward, the way the product actually works, with
   every module hanging off one core.

   Everything is CSS. A dedicated animation library would need a script tag
   from a CDN, which the Content-Security-Policy refuses, and would cost
   more over the wire than the whole page.
   ========================================================================== */

@keyframes auth-rise {
  from { opacity: 0; transform: translateY(10px); }
  to   { opacity: 1; transform: translateY(0); }
}

@keyframes auth-fade {
  from { opacity: 0; }
  to   { opacity: 1; }
}

@keyframes lock-spin {
  from { transform: rotate(0); }
  to   { transform: rotate(360deg); }
}

@keyframes lock-breathe {
  0%, 100% { opacity: .5; transform: scale(1); }
  50%      { opacity: .9; transform: scale(1.045); }
}

.auth.form-first .form-side .panel {
  animation: auth-rise .42s var(--ease, cubic-bezier(.22,.8,.3,1)) both;
}

.art-side .art-mark { animation: auth-fade .5s ease-out .10s both; }
.art-side .art-wordmark { animation: auth-rise .5s ease-out .14s both; }
.art-side .art-slogan { animation: auth-rise .5s ease-out .2s both; }
.art-side .art-lede h2 { animation: auth-rise .5s ease-out .16s both; }
.art-side .art-lede p  { animation: auth-rise .5s ease-out .22s both; }
.art-side .built-by    { animation: auth-fade .6s ease-out .55s both; }
.art-side .art-glow    { animation: auth-fade 1.1s ease-out both; }

/* ------------------------------------------------------- signed out page */

@keyframes lock-shut {
  from { transform: translateY(-5px) scaleY(1.14); opacity: 0; }
  to   { transform: translateY(0) scaleY(1); opacity: 1; }
}

@keyframes lock-ring-out {
  from { opacity: 0; transform: scale(.72); }
  to   { opacity: 1; transform: scale(1); }
}

.signed-out .lock-mark .body,
.signed-out .lock-mark .pin {
  animation: auth-fade .4s ease-out .1s both;
}
/* The shackle drops shut a beat after the body lands. It is the one moment
   of motion on the page and it says the thing the page is for. */
.signed-out .lock-mark .shackle {
  transform-origin: 24px 28px;
  animation: lock-shut .5s cubic-bezier(.3,1.3,.5,1) .3s both;
}
.signed-out .lock-track {
  animation: lock-ring-out .6s ease-out .18s both,
             lock-spin 60s linear .8s infinite;
}
.signed-out .lock-ring.two {
  animation: lock-ring-out .6s ease-out .3s both,
             lock-breathe 4s ease-in-out 1s infinite;
}
.signed-out .lock-panel > h1 { animation: auth-rise .45s ease-out .34s both; }
.signed-out .lock-panel > .sub { animation: auth-rise .45s ease-out .4s both; }
.signed-out .lock-panel > .btn { animation: auth-rise .45s ease-out .48s both; }
.signed-out .lock-panel > .closing { animation: auth-fade .5s ease-out .6s both; }

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

   Every animated part has to be named. An animation left running would be
   the problem; one merely shortened to nothing while it still has a "from"
   keyframe of opacity 0 leaves the element invisible - which is worse than
   the animation, and specifically worse for the people who asked. */

@media (prefers-reduced-motion: reduce) {
  .auth.form-first .form-side .panel,
  .art-side .art-mark,
  .art-side .art-wordmark,
  .art-side .art-slogan,
  .art-side .art-lede h2,
  .art-side .art-lede p,
  .art-side .built-by,
  .art-side .art-glow,
  .core-bloom,
  .core-rings,
  .core-hex,

  .core-mark,
  .core-links .link,
  .core-links .pulse,
  .signed-out .lock-mark .body,
  .signed-out .lock-mark .pin,
  .signed-out .lock-mark .shackle,
  .signed-out .lock-track,
  .signed-out .lock-ring.two,
  .signed-out .lock-panel > h1,
  .signed-out .lock-panel > .sub,
  .signed-out .lock-panel > .btn,
  .signed-out .lock-panel > .closing {
    animation: none !important;
    opacity: 1 !important;
    transform: none !important;
    stroke-dashoffset: 0 !important;
  }
  .btn.signin:hover svg { transform: none; }
}

/* ==========================================================================
   v12 shell: the collapsible rail
   ==========================================================================
   Collapsing is driven by an attribute on <html>, not a class on the
   sidebar, for one reason: the attribute is set by the inline script in
   <head> before the first paint. Anything keyed to an element deeper in the
   document could not be applied until that element existed, which is after
   layout has already been done at the wrong width.

   Everything that has to disappear in the rail carries .rail-hide, so the
   rule below is the only place that decides what a rail is. Nav labels,
   group headings and the tenant name all go; the icons and the active
   marker stay, because those are what someone is steering by. */

[data-rail="1"] .sidebar {
  width: var(--sidebar-rail);
  flex-basis: var(--sidebar-rail);
}
[data-rail="1"] .sidebar .rail-hide { display: none; }

[data-rail="1"] .sidebar .brand { padding: 18px 0 14px; }
/* Belt and braces: nothing in the brand block may reach outside the rail,
   whatever aspect ratio a future logo file turns out to have. The rest of
   the sidebar is left unclipped because the rail's tooltips are drawn
   outside it on purpose. */
.sidebar .brand { overflow: hidden; }
[data-rail="1"] .sidebar .brandmark { align-items: center; gap: 0; }
[data-rail="1"] .sidebar .brandmark .logo { height: 34px; }
[data-rail="1"] .sidebar .tenant { justify-content: center; margin-top: 12px; }

[data-rail="1"] .sidebar nav { padding: 10px 8px 20px; }
[data-rail="1"] .nav-group > .eyebrow { display: none; }
/* A divider replaces the group heading. Without it every icon in the rail
   sits in one undifferentiated column and the grouping - which is the only
   structure the navigation has - is lost entirely. */
[data-rail="1"] .nav-group + .nav-group {
  margin-top: 10px; padding-top: 10px;
  border-top: 1px solid rgba(255, 255, 255, .08);
}
[data-rail="1"] .sidebar nav a {
  justify-content: center;
  padding: 10px 0;
  position: relative;
}
[data-rail="1"] .sidebar nav a svg { width: 19px; height: 19px; }
/* The unread count has no room to sit inline, so it becomes a pip on the
   icon - the same device the topbar bell already uses. */
[data-rail="1"] .sidebar nav a .count {
  position: absolute; top: 4px; right: 12px;
  margin-left: 0; min-width: 8px; height: 8px; padding: 0;
  font-size: 0; line-height: 0;
  background: var(--brand-400);
}
[data-rail="1"] .sidebar .foot { padding: 12px 8px; }
[data-rail="1"] .sidebar .foot .who { justify-content: center; }
[data-rail="1"] .sidebar .foot .avatar { width: 36px; height: 36px; font-size: .82rem; }

.sidebar .foot .who {
  display: flex; align-items: center; gap: 10px; min-width: 0;
}
.sidebar .foot .who .avatar { width: 38px; height: 38px; font-size: .85rem; }
.sidebar .foot .who strong { display: block; }

/* The rail tooltip. Built here rather than left to the browser's title
   attribute, which waits about a second before appearing - long enough that
   someone scanning a column of unlabelled icons gives up and reopens the
   sidebar, which is the one outcome a rail exists to avoid. */
/* The rail tooltip is the browser's own now.
   
   It used to be a ::after positioned past the rail's right edge. That is
   outside the scroll container above, which clips - so the choice was a
   tooltip nobody could see or a sidebar that scrolled sideways and
   overlapped the page. The native title attribute costs about a second
   before it appears, which is worse than the old one and much better than
   either of those. */


.rail-toggle svg { transition: transform .18s var(--ease); }
[data-rail="1"] .rail-toggle svg { transform: rotate(180deg); }

/* ==========================================================================
   v12 shell: breadcrumb and user menu
   ========================================================================== */

.topbar .crumb {
  display: flex; align-items: center; gap: 8px;
  font-weight: 600; font-size: .93rem; min-width: 0;
}
.topbar .crumb a {
  color: var(--text); text-decoration: none; white-space: nowrap;
  overflow: hidden; text-overflow: ellipsis; max-width: 30ch;
}
.topbar .crumb a:hover { color: var(--brand-700); }
.topbar .crumb .sep { color: var(--text-3); font-weight: 400; }
.topbar .crumb .here { color: var(--text-2); font-weight: 500; }
.topbar .crumb form { display: flex; }
/* The switcher reads as part of the trail, not as a form control dropped
   next to it: no border until it is hovered, so the topbar stays quiet. */
.topbar .crumb select {
  border-color: transparent;
  background-color: transparent;
  font-weight: 600;
  padding: 4px 26px 4px 8px;
  height: 32px;
  color: var(--text-2);
  cursor: pointer;
}
.topbar .crumb select:hover {
  background-color: var(--surface-3); border-color: var(--line);
}

/* A generic dropdown. Used by the user menu and by the overflow menu on a
   table row, so both behave identically - opening, closing on Escape or an
   outside click, and being reachable from the keyboard. */
.menu { position: relative; }
.menu-panel {
  position: absolute; right: 0; top: calc(100% + 8px);
  min-width: 216px;
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  box-shadow: var(--shadow-2);
  padding: 6px;
  z-index: 70;
  display: none;
}
.menu.open .menu-panel { display: block; }

/* An open panel that app.js has moved to <body> - see the note there about
   transformed ancestors and `.table-wrap` clipping row menus. Once
   re-parented it is no longer a descendant of `.menu.open`, so it cannot be
   revealed by that rule and carries its own class instead.

   It sits above the toast strip. Toasts are transient and outrank most
   things; they do not outrank the thing that was deliberately opened a
   moment ago and would otherwise be hidden by one. */
.menu-panel.floating { display: block; z-index: 210; }

/* A long menu - the per-academy feature list is one item per gateable
   module - scrolls inside itself. app.js sets the exact ceiling from the
   room actually available; this is the fallback when it has not run. */
.menu-panel { max-height: min(60vh, 460px); overflow-y: auto; }

/* Menu items are one line. The feature menu carries module names long
   enough to wrap at the old 216px, and a wrapped item with its tick sitting
   beside the first line reads as a rendering fault. */
.menu-panel a, .menu-panel button { white-space: nowrap; }
.menu-panel .menu-head {
  padding: 8px 10px 10px; border-bottom: 1px solid var(--line);
  margin-bottom: 6px; display: grid; gap: 1px;
}
.menu-panel .menu-head strong { font-size: .9rem; }
.menu-panel .menu-head span { font-size: .78rem; color: var(--text-2); }
.menu-panel a,
.menu-panel button {
  display: flex; align-items: center; gap: 9px; width: 100%;
  padding: 8px 10px;
  border: 0; background: none; text-align: left;
  font: inherit; font-size: .875rem; color: var(--text);
  border-radius: var(--radius-sm);
  text-decoration: none; cursor: pointer;
}
.menu-panel a:hover, .menu-panel button:hover { background: var(--surface-3); }
.menu-panel svg { width: 16px; height: 16px; color: var(--text-2); flex: none; }
.menu-panel button.danger { color: var(--danger); }
.menu-panel button.danger svg { color: var(--danger); }
.menu-panel form { margin: 0; }
.menu-panel .sep {
  height: 1px; background: var(--line); margin: 6px 2px;
}

.user-chip {
  display: flex; align-items: center; gap: 8px;
  padding: 4px 8px 4px 4px;
  border: 1px solid transparent;
  background: none;
  border-radius: 40px;
  cursor: pointer;
  font: inherit;
  color: var(--text);
  max-width: 220px;
}
.user-chip:hover { background: var(--surface-3); border-color: var(--line); }
.user-chip .avatar { width: 30px; height: 30px; border-radius: 50%; font-size: .72rem; }
.user-chip .name {
  font-size: .86rem; font-weight: 600;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.user-chip svg { width: 15px; height: 15px; color: var(--text-3); flex: none; }

/* The overflow menu on a table row. The actions themselves are unchanged -
   they are simply not all shown at once, because a row with five buttons on
   it makes the row about the buttons rather than about the player. */
.row-menu .icon-btn { border-color: transparent; }
.row-menu .icon-btn:hover { border-color: var(--line); }
.row-menu .menu-panel { min-width: 190px; }
.row-menu .menu-panel button[type="submit"] { color: var(--danger); }

/* ==========================================================================
   v12: stat tiles with an icon and a trend
   ========================================================================== */

.stat .stat-top {
  display: flex; align-items: flex-start; justify-content: space-between;
  gap: 10px;
}
.stat .stat-icon {
  width: 34px; height: 34px; border-radius: 10px; flex: none;
  display: grid; place-items: center;
  background: var(--brand-050); color: var(--brand-700);
}
.stat .stat-icon svg { width: 17px; height: 17px; }
.stat.good .stat-icon { background: var(--success-050); color: var(--success); }
.stat.warn .stat-icon { background: var(--warning-050); color: var(--warning); }
.stat.bad .stat-icon { background: var(--danger-050); color: var(--danger); }
.stat.flat .stat-icon { background: var(--slate-050); color: var(--slate); }

.stat .stat-trend {
  display: inline-flex; align-items: center; gap: 4px;
  font-size: .8rem; font-weight: 650; margin-top: 4px;
}
.stat .stat-trend.up { color: var(--success); }
.stat .stat-trend.down { color: var(--danger); }
.stat .stat-trend .since { color: var(--text-3); font-weight: 500; }

/* ==========================================================================
   Player photographs
   ==========================================================================
   The avatar is one component with two faces: initials when there is no
   photo, the photo when there is. Keeping it one component is what stops a
   roster where some rows have a round portrait and others have a square
   block of initials. */

.avatar {
  overflow: hidden;
  position: relative;
}
.avatar img {
  position: absolute; inset: 0;
  width: 100%; height: 100%;
  object-fit: cover;
  display: block;
}
.avatar.sm { width: 34px; height: 34px; border-radius: 9px; font-size: .74rem; }

/* The photo panel on a player's page. Sized to the ID card's photo well so
   what is on screen is what will print - a portrait that looks right in a
   square frame and wrong on the card is a trap worth designing out. */
.photo-panel {
  display: flex; gap: 18px; align-items: flex-start; flex-wrap: wrap;
  padding: 18px;
}
.photo-frame {
  width: 132px; height: 161px;
  border-radius: var(--radius);
  border: 1px solid var(--line);
  background: var(--surface-3);
  overflow: hidden; flex: none; position: relative;
  display: grid; place-items: center;
}
.photo-frame img { width: 100%; height: 100%; object-fit: cover; display: block; }
.photo-frame .none {
  display: grid; gap: 6px; justify-items: center;
  color: var(--text-3); font-size: .74rem; font-weight: 700;
  letter-spacing: .08em; text-transform: uppercase;
}
.photo-frame .none svg { width: 26px; height: 26px; }
.photo-side { flex: 1; min-width: 220px; }
.photo-side p { margin: 0 0 12px; color: var(--text-2); font-size: .86rem; }
.photo-side .row-actions { justify-content: flex-start; margin-top: 10px; }
/* The file input is replaced by a button that opens it. The native control
   renders differently in every browser and cannot be styled to match the
   rest of the form, so it is hidden and driven by its own label. */
.file-pick input[type="file"] {
  position: absolute; width: 1px; height: 1px;
  padding: 0; margin: -1px; overflow: hidden;
  clip: rect(0 0 0 0); white-space: nowrap; border: 0;
}
.file-pick { position: relative; display: inline-flex; }
.file-name {
  font-size: .82rem; color: var(--text-2); align-self: center;
  max-width: 22ch; overflow: hidden; text-overflow: ellipsis;
  white-space: nowrap;
}

/* A cell that pairs a portrait with a name, used in the roster table. */
.who-cell { display: flex; align-items: center; gap: 10px; min-width: 0; }
.who-cell .stack { min-width: 0; }
.who-cell a { text-decoration: none; color: var(--text); }
.who-cell a:hover strong { color: var(--brand-700); }

/* ==========================================================================
   v12 responsive: the rail is a laptop idea, not a phone one
   ==========================================================================
   Below 760px the sidebar is already a drawer that slides over the page, so
   a collapsed-but-present rail would be a third state with no purpose. The
   attribute is left alone - it is remembered for when the window is wide
   again - and simply not honoured here. */

@media (max-width: 900px) {
  .user-chip .name { display: none; }
}

@media (max-width: 760px) {
  .rail-toggle { display: none; }
  [data-rail="1"] .sidebar {
    width: var(--sidebar-w);
    flex-basis: var(--sidebar-w);
  }
  [data-rail="1"] .sidebar .rail-hide { display: revert; }
  /* Below this width the rail is not collapsed, so the brand band matches
     the expanded sidebar exactly. It used to restore the old stacked
     layout - logo, rule, "AcademyOS" - which no longer exists. */
  [data-rail="1"] .sidebar .brand { padding: 16px 18px 16px 20px; }
  [data-rail="1"] .sidebar .brandmark { align-items: center; gap: 0; }
  [data-rail="1"] .sidebar .tenant { justify-content: flex-start; }
  [data-rail="1"] .nav-group > .eyebrow { display: block; }
  [data-rail="1"] .nav-group + .nav-group { border-top: 0; padding-top: 0; }
  [data-rail="1"] .sidebar nav a { justify-content: flex-start; padding: 8px 10px; }
  [data-rail="1"] .sidebar nav a::after { display: none; }
  [data-rail="1"] .sidebar nav a .count {
    position: static; margin-left: auto;
    min-width: 20px; height: auto; padding: 1px 7px;
    font-size: .7rem; line-height: 1.4;
    background: rgba(255, 255, 255, .14);
  }
  [data-rail="1"] .sidebar .foot .who { justify-content: flex-start; }

  .topbar { gap: 8px; padding: 0 12px; }
  .topbar .crumb a { max-width: 14ch; }
  .photo-panel { padding: 14px; }
  .menu-panel { min-width: 200px; }
}

/* ==========================================================================
   Standard shells - KPI, attention list, skeleton, error state

   These sit at the end deliberately: they are additions to the system rather
   than overrides of it, and nothing above needs to know they exist.
   ========================================================================== */

/* ------------------------------------------------------------------- KPI */

.page-head + .kpi-row, .workflow + .kpi-row { margin-top: 0; }

.kpi-row {
  /* Follows a card as often as it opens a page, and a grid is not a .card,
     so the `.card + .card` rule that spaces everything else did not apply. */
  margin-top: var(--gap-section);
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(190px, 1fr));
  gap: 14px;
  margin-bottom: var(--gap-section);
}

.kpi {
  position: relative;
  display: block;
  padding: 15px 16px 16px;
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  box-shadow: var(--shadow-1);
  overflow: hidden;
  color: inherit;
  text-decoration: none;
  transition: box-shadow .16s var(--ease), transform .16s var(--ease),
              border-color .16s var(--ease);
}
/* The spine. A colour-blind reader gets the same information from the label
   and the note, so this repeats a signal rather than carrying it alone. */
.kpi::before {
  content: "";
  position: absolute; left: 0; top: 0; bottom: 0;
  width: 3px;
  background: var(--brand-600);
}
.kpi.good::before { background: var(--success); }
.kpi.warn::before { background: var(--warning); }
.kpi.bad::before  { background: var(--danger); }
.kpi.flat::before { background: var(--line-strong); }
.kpi.accent::before { background: var(--accent); }

a.kpi:hover {
  box-shadow: var(--shadow-2);
  transform: translateY(-1px);
  border-color: var(--line-strong);
  text-decoration: none;
}
a.kpi:focus-visible { outline: 2px solid var(--brand-600); outline-offset: 2px; }

.kpi-top { display: flex; align-items: center; gap: 8px; }
.kpi-label {
  font-size: .72rem;
  font-weight: 700;
  letter-spacing: .07em;
  text-transform: uppercase;
  color: var(--text-3);
}
.kpi-icon {
  margin-left: auto;
  display: inline-flex;
  width: 26px; height: 26px;
  align-items: center; justify-content: center;
  border-radius: 7px;
  background: var(--surface-3);
  color: var(--text-2);
}
.kpi-icon svg { width: 14px; height: 14px; }
.kpi.good .kpi-icon { background: var(--success-050); color: var(--success-ink); }
.kpi.warn .kpi-icon { background: var(--warning-050); color: var(--warning-ink); }
.kpi.bad  .kpi-icon { background: var(--danger-050);  color: var(--danger-ink); }
.kpi.accent .kpi-icon { background: var(--accent-050); color: var(--accent-ink); }

.kpi-value {
  margin-top: 6px;
  font-size: 1.85rem;
  font-weight: 700;
  line-height: 1.1;
  letter-spacing: -.02em;
  font-variant-numeric: tabular-nums;
}
.kpi-value .unit {
  font-size: .95rem;
  font-weight: 600;
  color: var(--text-3);
  margin-left: 2px;
}
.kpi-delta {
  margin-top: 5px;
  font-size: .78rem;
  font-weight: 650;
  color: var(--text-2);
  display: inline-flex; align-items: center; gap: 4px;
}
/* Direction is drawn, not coloured by assumption: an arrow says which way
   the number moved and the caller decides whether that is good news. */
.kpi-delta.up::before   { content: "\2191"; }
.kpi-delta.down::before { content: "\2193"; }
.kpi-delta.up   { color: var(--success-ink); }
.kpi-delta.down { color: var(--danger-ink); }
.kpi-delta.up.inverse   { color: var(--danger-ink); }
.kpi-delta.down.inverse { color: var(--success-ink); }

.kpi-note { margin-top: 4px; font-size: .8rem; color: var(--text-2); }
.kpi .meter { margin-top: 10px; }

/* ------------------------------------------------------- Needs attention */

.attn-list { display: flex; flex-direction: column; }
.attn {
  display: flex;
  align-items: center;
  gap: 11px;
  padding: 11px 16px;
  border-bottom: 1px solid var(--line);
  color: inherit;
  text-decoration: none;
  transition: background .13s var(--ease);
}
.attn:last-child { border-bottom: 0; }
.attn:hover { background: var(--surface-2); text-decoration: none; }
.attn:focus-visible { outline: 2px solid var(--brand-600); outline-offset: -2px; }

.attn-mark {
  flex: none;
  width: 30px; height: 30px;
  display: inline-flex; align-items: center; justify-content: center;
  border-radius: 8px;
  background: var(--slate-050); color: var(--slate-ink);
}
.attn-mark svg { width: 16px; height: 16px; }
.attn.warn .attn-mark { background: var(--warning-050); color: var(--warning-ink); }
.attn.bad  .attn-mark { background: var(--danger-050);  color: var(--danger-ink); }
.attn.good .attn-mark { background: var(--success-050); color: var(--success-ink); }
.attn.brand .attn-mark { background: var(--brand-050);  color: var(--brand-700); }

.attn-body { min-width: 0; display: flex; flex-direction: column; }
.attn-body strong { font-size: .875rem; font-weight: 620; }
.attn-blurb {
  font-size: .78rem; color: var(--text-2);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.attn-go { margin-left: auto; color: var(--text-3); flex: none; }
.attn-go svg { width: 16px; height: 16px; }
.attn:hover .attn-go { color: var(--brand-600); }

/* An all-clear is worth drawing. A panel that vanishes when there is nothing
   wrong makes the reader wonder whether it failed to load. */
.attn-clear {
  display: flex; align-items: center; gap: 10px;
  padding: 16px;
  color: var(--text-2); font-size: .85rem;
}
.attn-clear .mark {
  width: 30px; height: 30px; flex: none;
  display: inline-flex; align-items: center; justify-content: center;
  border-radius: 8px;
  background: var(--success-050); color: var(--success-ink);
}
.attn-clear .mark svg { width: 16px; height: 16px; }

/* -------------------------------------------------------------- skeleton */

.skeleton { padding: 14px 16px; display: flex; flex-direction: column; gap: 10px; }
.skeleton i {
  display: block; height: 11px; border-radius: 5px;
  background: linear-gradient(90deg, var(--surface-3) 25%,
              var(--surface-2) 37%, var(--surface-3) 63%);
  background-size: 400% 100%;
  animation: shimmer 1.4s ease infinite;
}
.skeleton i:nth-child(2n) { width: 82%; }
.skeleton i:nth-child(3n) { width: 64%; }
@keyframes shimmer { from { background-position: 100% 50%; }
                     to   { background-position: 0 50%; } }

/* ----------------------------------------------------------- error state */

.empty.error .mark { background: var(--danger-050); color: var(--danger-ink); }

/* Motion is decoration here, so it goes when it is not wanted. */
@media (prefers-reduced-motion: reduce) {
  .skeleton i { animation: none; }
  a.kpi:hover { transform: none; }
}

/* ==========================================================================
   Player hero - the flagship screen's signature

   A player is the thing this product is about, so their page opens with the
   one artefact the sport already uses to introduce a player: the squad card.
   Navy field, the shirt number set enormous and half-faded behind the
   portrait, and the form guide sitting on the card rather than three
   sections down the page.

   The number is the risk worth taking here. It is decorative at 150px and
   would be indefensible on an admin screen - but this is not one, and a
   roster page where every player looks like a table row is a roster page
   nobody feels anything about.
   ========================================================================== */

.player-hero {
  position: relative;
  overflow: hidden;
  border-radius: var(--radius);
  background:
    radial-gradient(120% 140% at 88% 0%, rgba(6, 182, 212, .28) 0%,
                    rgba(6, 182, 212, 0) 55%),
    linear-gradient(135deg, var(--navy-800) 0%, var(--navy-900) 62%);
  color: var(--on-navy);
  border: 1px solid var(--navy-700);
  box-shadow: var(--shadow-2);
  margin-bottom: var(--gap-section);
}

/* The shirt number. aria-hidden in the markup - it repeats the number that
   is already in the pill row, and a screen reader announcing "seven" twice
   with no context between them is noise. */
.player-hero .shirt {
  position: absolute;
  /* Anchored to the portrait end of the card, not the right edge: on the
     right it sat underneath the action buttons, and a numeral showing
     through a button is a numeral that looks like a rendering fault. */
  left: 46%; top: 50%;
  transform: translateY(-50%);
  font-size: 9.5rem;
  font-weight: 800;
  line-height: 1;
  letter-spacing: -.05em;
  color: #fff;
  opacity: .07;
  pointer-events: none;
  user-select: none;
  font-variant-numeric: tabular-nums;
}

.player-hero .inner {
  position: relative;
  display: flex;
  align-items: center;
  gap: 20px;
  padding: 22px 24px;
  flex-wrap: wrap;
}

.player-hero .portrait {
  width: 96px; height: 96px;
  flex: none;
  border-radius: 16px;
  overflow: hidden;
  background: var(--navy-700);
  border: 2px solid rgba(255, 255, 255, .14);
  display: grid; place-items: center;
  font-size: 2rem; font-weight: 700; color: #fff;
  box-shadow: 0 8px 24px rgba(0, 0, 0, .3);
}
.player-hero .portrait img { width: 100%; height: 100%; object-fit: cover; }

.player-hero .who { min-width: 0; flex: 1 1 260px; }
.player-hero h1 {
  margin: 0;
  font-size: 1.7rem;
  letter-spacing: -.02em;
  color: #fff;
}
.player-hero .code {
  font-family: var(--font-data);
  font-size: .8rem;
  color: var(--brand-400);
  letter-spacing: .04em;
}
.player-hero .pill-row { margin-top: 9px; }
/* Badges sit on navy here, so the light tints they normally use would
   disappear. They get a translucent fill instead of a second set of tokens. */
.player-hero .badge {
  background: rgba(255, 255, 255, .12);
  color: #fff;
}
.player-hero .badge.good { background: rgba(16, 185, 129, .22); color: #6EE7B7; }
.player-hero .badge.warn { background: rgba(245, 158, 11, .22); color: #FCD34D; }
.player-hero .badge.bad  { background: rgba(239, 68, 68, .22);  color: #FCA5A5; }
.player-hero .badge.brand { background: rgba(37, 99, 235, .3);  color: #93C5FD; }

/* The form guide, on the card. It is the first thing a coach looks for. */
.player-hero .form-guide {
  display: flex; flex-direction: column; gap: 6px;
  flex: none;
  padding-right: 8px;
}
.player-hero .form-guide .cap {
  font-size: .66rem; font-weight: 700; letter-spacing: .1em;
  text-transform: uppercase; color: var(--on-navy-2);
}
.player-hero .strip i.excused, .player-hero .strip i.none {
  border-color: rgba(255, 255, 255, .3); color: var(--on-navy-2);
}

.player-hero .hero-actions {
  display: flex; gap: 8px; flex-wrap: wrap;
  margin-left: auto;
}
.player-hero .btn.secondary {
  background: rgba(255, 255, 255, .1);
  border-color: rgba(255, 255, 255, .18);
  color: #fff;
}
.player-hero .btn.secondary:hover { background: rgba(255, 255, 255, .18); }

@media (max-width: 760px) {
  .player-hero .shirt { font-size: 6rem; opacity: .06; left: auto; right: 14px; }
  .player-hero .inner { padding: 18px; gap: 14px; }
  .player-hero .portrait { width: 68px; height: 68px; font-size: 1.5rem; }
  .player-hero h1 { font-size: 1.35rem; }
  .player-hero .hero-actions { margin-left: 0; width: 100%; }
}

/* Rank column on leaderboards */
td.rank {
  width: 26px;
  font-family: var(--font-data);
  font-size: .78rem;
  font-weight: 700;
  color: var(--text-3);
  text-align: right;
}

/* A date-and-time cell that stacks rather than running on one line */
.when { display: flex; flex-direction: column; line-height: 1.25; }

/* Announcement list on the dashboard */
.note-list { display: flex; flex-direction: column; }
.note {
  display: block; padding: 11px 16px;
  border-bottom: 1px solid var(--line);
  color: inherit; text-decoration: none;
}
.note:last-child { border-bottom: 0; }
.note:hover { background: var(--surface-2); text-decoration: none; }
.note-head { display: flex; gap: 8px; align-items: baseline; }
.note-head strong { font-size: .85rem; }
.note-body { font-size: .8rem; color: var(--text-2); margin-top: 2px; }

/* ------------------------------------------------- account & tenant chrome */

/* A state tag inside a menu item - "On" / "Off" against the authenticator,
   so the menu answers the question without being opened. */
.menu-panel .menu-tag {
  margin-left: auto;
  padding: 1px 7px;
  border-radius: 20px;
  font-size: .68rem;
  font-weight: 700;
  letter-spacing: .02em;
}
.menu-panel .menu-tag.good { background: var(--success-050); color: var(--success-ink); }
.menu-panel .menu-tag.warn { background: var(--warning-050); color: var(--warning-ink); }

.sidebar .tenant .names { display: flex; flex-direction: column; line-height: 1.2; min-width: 0; }
.sidebar .tenant .names strong {
  font-size: .82rem; font-weight: 620; color: var(--sidebar-text);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.sidebar .tenant .names .mono {
  font-size: .66rem; color: var(--sidebar-muted); letter-spacing: .06em;
  text-transform: uppercase;
}

/* ---------------------------------------------------------- workflow bar */

.workflow {
  display: flex;
  align-items: center;
  gap: 4px;
  flex-wrap: wrap;
  margin-bottom: var(--gap-section);
  padding: 5px;
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  box-shadow: var(--shadow-1);
}
.workflow a, .workflow .soon {
  padding: 7px 13px;
  border-radius: var(--radius-sm);
  font-size: .82rem;
  font-weight: 600;
  color: var(--text-2);
  text-decoration: none;
  white-space: nowrap;
}
.workflow a:hover { background: var(--surface-3); color: var(--text); text-decoration: none; }
.workflow a.active { background: var(--brand-600); color: var(--on-brand); }
/* A stage that is not built yet. Shown, because a path that quietly stops
   two steps early reads as a path that ends there. */
.workflow .soon { color: var(--text-3); cursor: default; }
.workflow .soon::after {
  content: "soon";
  margin-left: 6px;
  padding: 1px 6px;
  border-radius: 20px;
  background: var(--surface-3);
  font-size: .62rem;
  font-weight: 700;
  letter-spacing: .04em;
  text-transform: uppercase;
}
/* No chevrons between the stages. The first attempt drew them with a
   ::before on each item, which puts the mark *inside* the pill - so it sat
   on the active tab's blue fill and inside its hover area, and a stray
   :not(:last-child) rule meant only the final stage got one anyway. Reading
   order and the "soon" tags carry the sequence without the decoration. */

/* ==========================================================================
   Packages and feature access

   Three surfaces share this block: the platform matrix, the academy's own
   subscription page, and the locked page someone reaches by following a
   link into a module their package does not include.
   ========================================================================== */

/* ------------------------------------------------------- the tier matrix */

.matrix { width: 100%; }
.matrix th.num, .matrix td.num { text-align: center; }

/* The package name and its price stack, so the column head reads as one
   thing rather than two rows that happen to line up. */
.matrix .matrix-package { display: block; font-weight: 680; }
.matrix thead th .faint { display: block; margin-top: 1px; font-weight: 500; }

/* A group heading inside the table body. A <th colspan> rather than a
   separate table per group, so the package columns stay aligned down the
   whole matrix - which is the only reason to draw a matrix at all. */
.matrix .matrix-group th {
  padding: 14px 16px 6px;
  background: var(--surface-2);
  font-size: .68rem;
  font-weight: 700;
  letter-spacing: .07em;
  text-transform: uppercase;
  color: var(--text-3);
  text-align: left;
}

/* Included, and included-and-cannot-be-removed. The core tick is drawn in
   the brand colour rather than green: green says "good", and what is being
   said here is "structural". */
.tick {
  display: inline-flex; align-items: center; justify-content: center;
  width: 24px; height: 24px;
  border-radius: 7px;
  background: var(--success-050); color: var(--success-ink);
}
.tick svg { width: 15px; height: 15px; }
.tick.core { background: var(--brand-050); color: var(--brand-700); }
/* Absent is an em dash, not a cross. A cross reads as an error; this is
   simply a module the tier does not carry. */
.tick.off { background: none; color: var(--text-3); }

/* A select that submits on change, sitting inside a table cell. */
.inline-set { display: inline-flex; gap: 6px; align-items: center; }
.inline-set select { min-width: 116px; }

/* --------------------------------------------- the academy's module list */

.module-group { padding: 4px 0 10px; }
.module-group > .eyebrow { display: block; padding: 10px 16px 6px; }

.module-list { list-style: none; margin: 0; padding: 0; }
.module-list li {
  display: flex; align-items: center; gap: 11px;
  padding: 9px 16px;
  border-top: 1px solid var(--line);
}
.module-list li:first-child { border-top: none; }

/* A locked row is dimmed rather than hidden. This is the one page where the
   reader has asked what they are missing, so showing it is the answer to
   their question rather than an advertisement. */
.module-list li.off { color: var(--text-3); }
.module-list li.off .module-body strong { font-weight: 560; }

.module-mark {
  width: 28px; height: 28px; flex: none;
  display: inline-flex; align-items: center; justify-content: center;
  border-radius: 8px;
  background: var(--success-050); color: var(--success-ink);
}
.module-mark svg { width: 15px; height: 15px; }
.module-list li.off .module-mark {
  background: var(--surface-3); color: var(--text-3);
}

.module-body { min-width: 0; display: flex; flex-direction: column; flex: 1; }
.module-body strong { font-size: .862rem; font-weight: 620; }
.module-body .sub { font-size: .76rem; color: var(--text-2); }
.module-list li.off .module-body .sub { color: var(--text-3); }
.module-list li .badge { flex: none; margin-left: auto; }

/* ------------------------------------------------------- usage against a limit */

.limit { padding: 4px 0 14px; }
.limit + .limit { border-top: 1px solid var(--line); padding-top: 14px; }
.limit-head {
  display: flex; align-items: baseline; justify-content: space-between;
  gap: 10px; margin-bottom: 8px;
}
.limit-head strong { font-size: .862rem; font-weight: 620; }
.limit-head .mono { font-size: .82rem; color: var(--text-2); }

/* Over the ceiling. An academy moved down a package is not deleted down to
   fit, so this state is ordinary rather than exceptional and is drawn as a
   condition of the row instead of an error banner elsewhere. */
.limit.over .limit-head .mono { color: var(--danger-ink); font-weight: 640; }
.limit-note { margin: 7px 0 0; font-size: .78rem; line-height: 1.45; }
.limit-note.bad { color: var(--danger-ink); }

/* ------------------------------------------------------- tier comparison */

.tier-list { display: flex; flex-direction: column; }
.tier { padding: 13px 16px; border-top: 1px solid var(--line); }
.tier:first-child { border-top: none; }
/* The tier the reader is on gets a spine, matching how .stat and .kpi mark
   their own tone - one device for "this one is yours" across the product. */
.tier.here {
  background: var(--brand-050);
  box-shadow: inset 3px 0 0 var(--brand-600);
}
.tier-head { display: flex; align-items: center; gap: 8px; }
.tier-head strong { font-size: .9rem; font-weight: 660; }
.tier .sub { display: block; margin-top: 3px; font-size: .8rem; color: var(--text-2); }
.tier .faint { display: block; margin-top: 2px; }

/* ------------------------------------------------------------ the locked page */

.locked-card { max-width: 560px; margin: 56px auto; }
.locked-card .body { text-align: center; padding: 36px 32px 32px; }
.locked-card h1 { margin: 6px 0 8px; font-size: 1.45rem; }
.locked-card .lead { font-size: .95rem; color: var(--text-2); margin: 0 0 12px; }
.locked-card .muted { font-size: .87rem; }
.locked-card .hint { margin-top: 16px; }

.locked-mark {
  display: inline-flex; align-items: center; justify-content: center;
  width: 46px; height: 46px;
  margin-bottom: 14px;
  border-radius: 13px;
  background: var(--warning-050); color: var(--warning-ink);
}
.locked-mark svg { width: 22px; height: 22px; }

.locked-actions {
  display: flex; gap: 10px; justify-content: center; flex-wrap: wrap;
  margin-top: 20px;
}

/* ------------------------------------------------------------------ mobile */

@media (max-width: 760px) {
  /* The matrix is the one table here that must not collapse to stacked
     rows: its whole purpose is comparing columns side by side, and a
     responsive stack turns it into three separate lists. It scrolls
     sideways instead, which keeps the comparison intact. */
  .matrix { min-width: 460px; }
  .matrix .matrix-group th { padding: 12px 12px 5px; }

  .locked-card { margin: 24px auto; }
  .locked-card .body { padding: 28px 20px 24px; }
  .locked-actions .btn { flex: 1 1 100%; }

  .module-list li { padding: 9px 12px; }
  .module-list li .badge { margin-left: 0; }
  /* At this width the label, the blurb and the badge cannot share a line
     without the blurb being cropped to nothing, so the badge wraps under. */
  .module-list li { flex-wrap: wrap; }
  .module-body { flex-basis: calc(100% - 39px); }
}

/* A state marker inside a menu item. Reserves its space whether or not it
   is ticked, so the labels align down the list instead of shifting left by
   16px on every item that happens to be off. */
.menu-tick {
  width: 16px; height: 16px; flex: none;
  display: inline-flex; align-items: center; justify-content: center;
}
.menu-tick.on { color: var(--success-ink); }
.menu-tick svg { width: 15px; height: 15px; color: inherit; }

/* ==========================================================================
   v15: academy logo, deep breadcrumbs, global search, confirmation modal
   ========================================================================== */

/* ------------------------------------------------------------ academy logo
   Replaces the coloured dot beside the academy name. Sized to the two lines
   of text next to it rather than to a round number, so the block has one
   optical height instead of a badge sitting proud of its own label.

   object-fit is contain and the plate is neutral: academy badges arrive as
   squares, circles and wide wordmarks, and cover would crop the crest off a
   wide one. A letterboxed logo is correct; a beheaded one is not. */
.sidebar .brand .tenant-logo {
  width: 30px; height: 30px; flex: none;
  border-radius: var(--radius-xs);
  object-fit: contain;
  background: rgba(255, 255, 255, .08);
  padding: 2px;
}
.sidebar .brand .tenant-logo.initials {
  display: grid; place-items: center;
  padding: 0;
  font-size: .72rem; font-weight: 700; letter-spacing: .04em;
  color: var(--brand-400);
  background: rgba(96, 165, 250, .14);
  border: 1px solid rgba(96, 165, 250, .22);
}
[data-rail="1"] .sidebar .brand .tenant-logo { width: 34px; height: 34px; }

/* ---------------------------------------------------------- deep crumbs
   The trail past the branch. It is the first thing to go when the topbar
   runs out of room - the academy and the branch answer "which tenant", the
   deep crumbs answer "which page", and the page itself already says that in
   its own heading. Losing the redundant half is better than wrapping the
   topbar onto two lines. */
.topbar .crumb .deep { font-weight: 500; }
.topbar .crumb a.deep { color: var(--text-2); max-width: 22ch; }
.topbar .crumb a.deep:hover { color: var(--brand-700); }
.topbar .crumb .here.deep { color: var(--text); font-weight: 600; }

/* ------------------------------------------------------------------ search */
.topbar .search {
  position: relative;
  display: flex; align-items: center; gap: 8px;
  width: min(360px, 34vw);
  height: 36px; padding: 0 10px;
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  background: var(--surface-2);
  transition: border-color .15s var(--ease), background .15s var(--ease);
}
.topbar .search:focus-within {
  border-color: var(--brand-600);
  background: var(--surface);
  box-shadow: 0 0 0 3px var(--brand-050);
}
.topbar .search svg { width: 16px; height: 16px; color: var(--text-3); flex: none; }
.topbar .search input {
  flex: 1; min-width: 0;
  border: 0; background: none; padding: 0; height: 100%;
  font-size: .9rem; color: var(--text);
}
.topbar .search input:focus { outline: none; box-shadow: none; }
.topbar .search input::-webkit-search-cancel-button { display: none; }

/* The shortcut hint. Hidden once there is text, because at that point it is
   telling you how to do the thing you are already doing. */
.topbar .search kbd {
  flex: none;
  font-family: var(--font-data); font-size: .68rem;
  color: var(--text-3);
  border: 1px solid var(--line); border-bottom-width: 2px;
  border-radius: var(--radius-xs);
  padding: 1px 5px;
  background: var(--surface);
}
.topbar .search:focus-within kbd { display: none; }

.search-panel {
  position: absolute; top: calc(100% + 6px); left: 0; right: 0;
  z-index: 60;
  max-height: min(420px, 60vh); overflow-y: auto;
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  box-shadow: var(--shadow-3);
  padding: 6px;
}
.search-panel[hidden] { display: none; }
.search-panel .group-label {
  font-size: .68rem; text-transform: uppercase; letter-spacing: .06em;
  color: var(--text-3); font-weight: 700;
  padding: 8px 10px 4px;
}
.search-panel a {
  display: flex; align-items: center; gap: 10px;
  padding: 8px 10px; border-radius: var(--radius-sm);
  text-decoration: none; color: var(--text);
}
.search-panel a:hover,
.search-panel a.active { background: var(--surface-3); }
.search-panel a svg { width: 16px; height: 16px; color: var(--text-3); flex: none; }
.search-panel .stack { min-width: 0; }
.search-panel .stack strong {
  display: block; font-size: .88rem; font-weight: 600;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.search-panel .stack .sub {
  display: block; font-size: .76rem; color: var(--text-3);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.search-panel .foot {
  border-top: 1px solid var(--line); margin-top: 6px; padding-top: 6px;
}
.search-panel .foot a { font-size: .82rem; color: var(--text-2); }
.search-panel .none { padding: 18px 12px; text-align: center; color: var(--text-3);
  font-size: .86rem; }

/* Results page groups. */
.search-group + .search-group { margin-top: var(--gap-section); }
.search-hit {
  display: flex; align-items: center; gap: 12px;
  padding: 12px 14px; border-radius: var(--radius-sm);
  text-decoration: none; color: inherit;
  border: 1px solid transparent;
}
.search-hit:hover { background: var(--surface-2); border-color: var(--line); }
.search-hit .mark {
  width: 34px; height: 34px; flex: none;
  display: grid; place-items: center; border-radius: var(--radius-sm);
  background: var(--surface-3); color: var(--text-3);
}
.search-hit .mark svg { width: 17px; height: 17px; }
.search-hit .stack { min-width: 0; flex: 1; }
.search-hit .stack strong { display: block; font-size: .94rem; }
.search-hit .stack .sub { font-size: .8rem; color: var(--text-3); }

/* ------------------------------------------------------- confirmation modal
   One instance, reused. It sits above the drawer rather than inside it,
   because a delete confirmed from inside a drawer must not be dismissed by
   the same click that closes the drawer behind it. */
.confirm-modal {
  position: fixed; inset: 0; z-index: 200;
  display: none; align-items: center; justify-content: center;
  padding: 20px;
}
.confirm-modal.open { display: flex; }
.confirm-modal .scrim {
  position: absolute; inset: 0;
  background: rgba(11, 17, 32, .55);
  -webkit-backdrop-filter: blur(2px);
  backdrop-filter: blur(2px);
  animation: fade .16s var(--ease);
}
.confirm-modal .panel {
  position: relative;
  width: min(420px, 100%);
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  box-shadow: var(--shadow-3);
  padding: 22px;
  text-align: center;
  animation: confirm-in .18s var(--ease);
}
.confirm-modal .mark {
  width: 44px; height: 44px; margin: 0 auto 12px;
  display: grid; place-items: center; border-radius: 50%;
  background: var(--danger-050); color: var(--danger-ink);
}
.confirm-modal .mark svg { width: 22px; height: 22px; }
.confirm-modal h2 { font-size: 1.08rem; margin: 0 0 6px; }
.confirm-modal p {
  margin: 0; color: var(--text-2); font-size: .9rem; line-height: 1.5;
}
.confirm-modal .guard { margin-top: 16px; text-align: left; }
.confirm-modal .guard[hidden] { display: none; }
.confirm-modal .guard strong { font-family: var(--font-data); }
.confirm-modal .actions {
  display: flex; gap: 10px; margin-top: 20px;
}
/* Equal width, cancel first. The safe action gets the reading position and
   neither gets the visual weight of being the wider button - a Delete twice
   the size of Cancel is a dialog with an opinion about what you should
   press. */
.confirm-modal .actions .btn { flex: 1; justify-content: center; }
.confirm-modal .actions .btn:disabled { opacity: .5; cursor: not-allowed; }

@keyframes confirm-in {
  from { opacity: 0; transform: translateY(8px) scale(.98); }
  to   { opacity: 1; transform: none; }
}

/* The page behind a modal must not scroll under it. */
body.modal-open { overflow: hidden; }

/* ------------------------------------------------------------ spotlight */
.spotlight-card .body { display: flex; align-items: center; gap: 16px; }
.spotlight-card .portrait {
  width: 64px; height: 64px; flex: none; border-radius: 50%;
  object-fit: cover; border: 2px solid var(--warning);
}
.spotlight-card .stack { min-width: 0; }
.spotlight-card .name { font-size: 1.05rem; font-weight: 700; }
.spotlight-card .citation {
  font-size: .86rem; color: var(--text-2); line-height: 1.5; margin-top: 4px;
}
.spotlight-card .laurel {
  display: inline-flex; align-items: center; gap: 5px;
  font-size: .7rem; text-transform: uppercase; letter-spacing: .06em;
  font-weight: 700; color: var(--warning-ink);
}
.spotlight-card .laurel svg { width: 14px; height: 14px; }

.candidate-row.ineligible { opacity: .55; }
.candidate-row .score {
  font-family: var(--font-data); font-weight: 700; color: var(--text);
}
.candidate-row.top .score { color: var(--success-ink); }

@media (max-width: 900px) {
  .topbar .search { width: 200px; }
  .topbar .search kbd { display: none; }
}

@media (max-width: 760px) {
  /* The deep trail goes before the search box does. Search is a tool you
     came to use; the crumb is a label you can read off the page heading. */
  .topbar .crumb .deep,
  .topbar .crumb .sep.deep { display: none; }
  .topbar .search { width: auto; flex: 1; min-width: 0; }
  .search-panel { position: fixed; left: 12px; right: 12px; top: calc(var(--topbar-h) + 8px); }
  .confirm-modal .actions { flex-direction: column-reverse; }
}

@media (prefers-reduced-motion: reduce) {
  .confirm-modal .panel,
  .confirm-modal .scrim { animation: none; }
}

/* --------------------------------------------------------------- chip row
   Filters that read as a row of toggles rather than a form. Used to narrow
   search results to one group; a select would hide the options behind a
   click, and the whole point is seeing which groups matched. */
.chip-row { display: flex; flex-wrap: wrap; gap: 8px; padding: 4px 0; }
.chip {
  display: inline-flex; align-items: center;
  padding: 5px 12px; border-radius: 999px;
  border: 1px solid var(--line);
  background: var(--surface-2);
  color: var(--text-2); font-size: .84rem; font-weight: 600;
  text-decoration: none;
}
.chip:hover { border-color: var(--line-strong); color: var(--text); }
.chip.on {
  background: var(--brand-600); border-color: var(--brand-600);
  color: var(--on-brand);
}

/* The skeleton inside the search panel. The bars are sized to the rows they
   stand in for - a result is a title over a subtitle, so a uniform bar
   would hold the wrong shape and the panel would still jump. */
.search-panel .skeleton { padding: 12px 10px; }
.search-panel .skeleton i { height: 12px; }

/* ==========================================================================
   Federated sign-in
   ========================================================================== */

/* The "or" divider between the password form and the provider buttons.
   A rule with a gap in it rather than a word floated over a border: the
   float version breaks the moment the panel background is not flat, which
   it is not once an academy sets its own colours. */
.or-rule {
  display: flex; align-items: center; gap: 12px;
  margin: 20px 0 16px; color: var(--text-3);
  font-size: .78rem; text-transform: uppercase; letter-spacing: .08em;
}
.or-rule::before, .or-rule::after {
  content: ""; flex: 1; height: 1px; background: var(--line);
}

.sso-buttons { display: grid; gap: 8px; }

/* Buttons carry a mark on the left and centre their label independently of
   it, so "Continue with Google" and "Continue with email" line up with each
   other rather than each being centred around its own icon width. */
.btn.sso {
  position: relative;
  display: flex; align-items: center; justify-content: center;
  gap: 10px; width: 100%;
  padding: 11px 14px;
  font-weight: 500;
  min-height: 44px;                 /* a comfortable touch target */
}
.btn.sso .sso-mark {
  position: absolute; left: 14px; top: 50%; transform: translateY(-50%);
  width: 18px; height: 18px; display: block; flex: none;
}
.btn.sso .sso-label { display: block; }

/* Google asks that its mark keeps its own colours on a light surface, so
   this button stays white in both themes rather than inheriting the
   surface token. The border does the work of separating it. */
.btn.sso.google { background: #fff; color: #1F1F1F; border-color: #DADCE0; }
.btn.sso.google:hover { background: #F7F8F8; border-color: #C9CDD1; }

/* Narrow phones: the absolute mark would collide with a wrapped label, so
   it goes back into the flow and the label stops being centred. */
@media (max-width: 380px) {
  .btn.sso { justify-content: flex-start; gap: 12px; }
  .btn.sso .sso-mark { position: static; transform: none; }
}

/* Recovery and authenticator codes. Two columns where there is room, one
   where there is not - a code that wraps mid-group is a code somebody
   transcribes wrong. */
.code-list {
  columns: 2; column-gap: 18px;
  margin: 12px 0; padding-left: 20px;
  line-height: 1.95; font-size: .84rem;
}
.code-list li { break-inside: avoid; }
@media (max-width: 480px) { .code-list { columns: 1; } }

.setup-secret {
  background: var(--surface); border: 1px solid var(--line);
  border-radius: var(--radius-sm); padding: 10px 12px;
  word-break: break-all; font-size: .85rem; margin: 8px 0 16px;
}
.qr-block { display: flex; justify-content: center; margin: 4px 0 14px; }
.qr-block svg { width: 172px; height: 172px; }

.small-heading {
  font-size: .9rem; margin: 18px 0 4px; font-weight: 600;
}

/* The linked-identity list on My account. */
.identity-row {
  display: flex; align-items: center; gap: 10px; flex-wrap: wrap;
  padding: 10px 0; border-bottom: 1px solid var(--line);
}
.identity-row:last-child { border-bottom: 0; }
.identity-row .grow { flex: 1; min-width: 140px; }

/* Aadhaar reveal control. The value is monospaced so the masked and
   revealed forms occupy the same width and the row does not jump when it
   changes. */
.aadhaar-fact { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
.aadhaar-fact .value { letter-spacing: .04em; }
.aadhaar-fact [data-aadhaar-timer] {
  font-variant-numeric: tabular-nums; white-space: nowrap;
}
@media (max-width: 480px) {
  .aadhaar-fact { align-items: flex-start; }
}

/* ==========================================================================
   Heat maps
   ========================================================================== */

.heatmap-scroll { overflow-x: auto; -webkit-overflow-scrolling: touch; }

.heatmap { border-collapse: separate; border-spacing: 2px; width: 100%; }
.heatmap th {
  font-size: .78rem; font-weight: 600; text-align: left;
  color: var(--text-2); padding: 6px 8px; white-space: nowrap;
}
.heatmap thead th { text-align: center; }
/* The row header stays put while the weeks scroll sideways - without it a
   wide range leaves you looking at numbers with no idea whose they are. */
.heatmap tbody th {
  position: sticky; left: 0; background: var(--surface); z-index: 1;
  min-width: 132px;
}
.heatmap tbody th .sub { display: block; font-weight: 400; }

.heatmap .cell {
  padding: 8px 10px; text-align: center; border-radius: var(--radius-sm);
  font-size: .8rem; font-variant-numeric: tabular-nums;
  background: var(--brand); color: #fff; min-width: 62px;
  cursor: default;
}
/* No data is drawn as absence, not as a low value. */
.heatmap .cell.none {
  background: transparent; color: var(--text-3);
  border: 1px dashed var(--line);
}

.heatmap-legend {
  display: flex; align-items: center; gap: 10px; margin-top: 14px;
  flex-wrap: wrap;
}
.heatmap-legend .scale {
  flex: 1; min-width: 120px; height: 10px; border-radius: 999px;
  background: linear-gradient(90deg,
    color-mix(in srgb, var(--brand) 12%, transparent),
    var(--brand));
}

.filter-row {
  display: flex; gap: 12px; align-items: flex-end; flex-wrap: wrap;
}
.filter-row .field { margin: 0; min-width: 140px; flex: 1 1 140px; }

@media (max-width: 640px) {
  .heatmap tbody th { min-width: 104px; font-size: .72rem; }
  .heatmap .cell { min-width: 48px; padding: 7px 6px; font-size: .72rem; }
}

/* ==========================================================================
   The sign-in card
   ========================================================================== */

/* The card carries the same design language as the artwork beside it -
   translucent surface, hairline border, generous radius, soft glow - so the
   two read as one composition rather than a form placed next to a picture.

   It is wider and more padded than the default .panel. A sign-in card is
   the first thing anyone sees of the product, and at 372px it looked like a
   utility dialog next to a full-height illustration. */

.auth.form-first .form-side .panel.signin-card {
  max-width: 452px;
  padding: 34px 34px 28px;
  border-radius: 22px;
  background:
    linear-gradient(168deg,
      color-mix(in srgb, var(--surface) 94%, transparent),
      color-mix(in srgb, var(--surface-2, var(--surface)) 88%, transparent));
  border: 1px solid var(--line);
  /* Two shadows: a wide soft one that lifts the card off the background,
     and a hairline inset that catches the top edge the way glass does. */
  box-shadow:
    0 28px 70px -30px rgba(0, 0, 0, .55),
    inset 0 1px 0 color-mix(in srgb, #fff 10%, transparent);
}

/* ---- the brand on the card --------------------------------------------- */

.signin-brand {
  display: flex;
  align-items: center;
  gap: 12px;
  margin-bottom: 26px;
}
/* The lockup is 11.3:3, so height alone sets its width. 44px, up from 34:
   on the photographic stage the card is larger and more of it is glass, and
   at 34 the mark read as a caption on the card rather than as the thing
   signing you in. */
.signin-brand img { height: 44px; width: auto; }

.auth.form-first .form-side .signin-card h1 {
  font-size: 1.72rem;
  font-weight: 700;
  letter-spacing: -.02em;
  margin-bottom: 6px;
}
.auth.form-first .form-side .signin-card .sub {
  font-size: .95rem;
  margin-bottom: 26px;
}

/* ---- inputs ------------------------------------------------------------ */

/* The icon sits inside the field rather than beside it, so the control is
   one object. Padding on the input makes room for it; positioning the icon
   over the text would put it on top of what somebody typed. */
.with-icon { position: relative; display: block; }
.with-icon > svg {
  position: absolute;
  left: 14px;
  top: 50%;
  transform: translateY(-50%);
  width: 18px;
  height: 18px;
  pointer-events: none;
  color: var(--text-3);
}
.with-icon > input { padding-left: 42px; }

.signin-card .field input {
  min-height: 48px;
  border-radius: 12px;
  background: color-mix(in srgb, var(--bg) 55%, transparent);
}
.signin-card .field input:focus {
  background: color-mix(in srgb, var(--bg) 78%, transparent);
}
.signin-card .field > span:first-child {
  font-size: .82rem;
  font-weight: 600;
  margin-bottom: 7px;
}

/* The reveal button and the leading icon share the field, so the button
   keeps its own side and the input leaves room at both ends. */
.with-reveal.with-icon > input { padding-left: 42px; padding-right: 44px; }

/* ---- remember me / forgot ---------------------------------------------- */

.signin-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  margin: 2px 0 22px;
}
.signin-row .check.remember {
  margin: 0;
  font-size: .88rem;
  color: var(--text-2);
}
.signin-row .forgot-link {
  font-size: .88rem;
  font-weight: 600;
  color: var(--brand-600);
  text-decoration: none;
  white-space: nowrap;
}
.signin-row .forgot-link:hover { text-decoration: underline; }

/* ---- the primary action ------------------------------------------------ */

.signin-card .btn.block.signin {
  min-height: 52px;
  border-radius: 12px;
  font-size: 1rem;
  font-weight: 650;
  /* The one gradient in the product, on the one control that matters most
     on this page. Everything else stays flat so this reads as the action. */
  background: linear-gradient(100deg, var(--brand-600), var(--core-ink));
  border-color: transparent;
  box-shadow: 0 12px 28px -14px color-mix(in srgb, var(--brand-600) 70%, transparent);
}
.signin-card .btn.block.signin:hover {
  filter: brightness(1.06);
}

/* ---- divider and federated buttons ------------------------------------- */

.signin-card .or-rule { margin: 22px 0 16px; }
.signin-card .btn.sso {
  min-height: 48px;
  border-radius: 12px;
}

/* ---- the closing line -------------------------------------------------- */

.signin-assurance {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  margin: 24px 0 0;
  padding-top: 18px;
  border-top: 1px solid var(--line);
  font-size: .82rem;
  color: var(--text-3);
}
.signin-assurance svg { width: 15px; height: 15px; color: var(--core-ink); flex: none; }

/* The reassurance line on the signed-out card. Larger than the assurance
   footer because on this page it is the message, not a footnote. */
.secured-note {
  display: flex;
  align-items: flex-start;
  gap: 10px;
  margin: 0 0 24px;
  padding: 14px 16px;
  border-radius: 12px;
  background: color-mix(in srgb, var(--core-ink) 8%, transparent);
  border: 1px solid var(--core-glass-edge);
  font-size: .88rem;
  color: var(--text-2);
}
.secured-note svg { width: 17px; height: 17px; color: var(--core-ink); flex: none; }

/* An anchor styled as the primary action needs the centring a button gets
   for free. */
a.btn.block.signin { text-decoration: none; }

/* ---- responsive -------------------------------------------------------- */

/* The card keeps its padding down to the point where the panel stacks. It
   is the thing somebody came for, so it is the last thing to give up
   space. */
@media (max-width: 640px) {
  .auth.form-first .form-side .panel.signin-card {
    max-width: 100%;
    padding: 26px 22px 22px;
    border-radius: 18px;
  }
  .auth.form-first .form-side .signin-card h1 { font-size: 1.5rem; }
  .signin-brand { margin-bottom: 20px; }
}

/* ==========================================================================
   Super Admin: the platform command centre
   ==========================================================================

   Every rule here is scoped to `.shell.platform`, which base.html sets only
   for the platform tier. That scoping is the whole design constraint: an
   academy admin, a coach, a parent and a player must keep the calmer
   product styling, and a rule written without the prefix would leak into
   all four the moment somebody reuses a class name.

   The platform tier gets the heavier treatment because it is a different
   job - operating the platform rather than running an academy - and
   because somebody who administers both should never be in doubt about
   which one they are looking at. */

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

/* These were `.shell.platform` - the platform tier only.
   
   That is why Super Admin and an Academy looked like different products
   while every design token matched: same variables, applied to one shell
   and not the other. The Academy side kept flat cards, plain hairlines and
   an untinted sidebar; the platform side had the wash, the tinted borders
   and the gradient surfaces.
   
   They are `.shell` now, so both roles get one design language. Nothing
   here is platform-specific - it is all surface treatment expressed in
   shared tokens. Genuinely platform-only components (the academy search,
   the platform empty states) keep their own scope below. */
.shell {
  /* A very faint pitch motif behind everything, fixed so it does not
     travel with the scroll. Two radial washes and a grid; at these
     opacities it is texture rather than an image, and it sits behind the
     content with no z-index games because it is painted on the shell
     itself. */
  background-image:
    radial-gradient(90% 60% at 78% -10%,
      color-mix(in srgb, var(--core-ink) 9%, transparent), transparent 70%),
    radial-gradient(70% 50% at 8% 108%,
      color-mix(in srgb, var(--brand-600) 7%, transparent), transparent 70%);
  background-attachment: fixed;
}

.shell.platform .sidebar {
  /* PLATFORM ONLY - deliberately not `.shell`.
  
     This was briefly promoted to every shell while unifying the surface
     treatments. That gave the academy sidebar the platform's brand tint,
     which reads as "the sidebar turned blue" against the plain navy it
     had before. The tint is what marks the platform tier as its own
     place; applying it everywhere removes the distinction and changes a
     panel nobody asked to change.
     
     The academy keeps the flat `--sidebar-bg` charcoal above. Both are
     built on the same charcoal now - the platform's only difference is a
     9% emerald wash at the top, which is enough to mark the tier without
     making it a different product. */
  /* Charcoal-based, NOT --surface.
     
     This was `color-mix(... var(--surface) ...)`, which is white in light
     mode - so the platform sidebar turned white while its text kept the
     on-navy tokens meant for a dark panel. #C6D2E0 on white is 1.53:1:
     legible only if you already knew what it said. The rest of the product
     keeps a navy sidebar in both themes and this one had quietly stopped
     matching it.
     
     Tinted toward the brand rather than plain navy, so the platform tier
     still reads as its own place - the difference is carried by the accent
     rail on the active item and this tint, not by inverting the panel. */
  /* The same charcoal and the same forest wash as the academy, carried
     further down the panel. That depth is the whole of the platform
     distinction now - not a different hue. */
  /* The same glass, washed deeper. Opaque stops here would paint over the
     translucency the base rail just established, and the platform would be
     the one place in the product where the material changed. */
  background:
    linear-gradient(180deg,
      color-mix(in srgb, var(--sidebar-forest) 92%, transparent) 0%,
      color-mix(in srgb, var(--sidebar-forest) 58%, var(--sidebar-bg)) 24%,
      color-mix(in srgb, var(--sidebar-bg) 88%, transparent) 60%);
  border-right-color: color-mix(in srgb, var(--core-ink) 22%, transparent);
}

/* The active item gets an illuminated rail rather than only a tint, which
   is what makes the current section findable at a glance down a list of
   eight. */
/* The platform's active item used to be overridden here. It is not any
   more, and the override is worth describing so it does not come back.

   It did three things wrong once the rail became glass. Its background was
   opaque, so the one item you are looking at was the one place the
   material stopped being translucent. Its ::before drew a second rail on
   top of the inset one the base rule already draws - with a 10px glow,
   which is the neon this treatment is meant to avoid. And overriding
   `background` alone dropped the inset edge that goes with it.

   Nothing replaced it because nothing needed to. The base rule was already
   saying "the active item is the same in both shells, it is the same
   product"; the override was saying it again, in a way that had drifted.
   The platform tier is marked by the deeper forest wash on the panel
   itself, which is a property of the surface rather than of one item on
   it. */

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

.shell .stat {
  position: relative;
  overflow: hidden;
  border-radius: 16px;
  background:
    linear-gradient(165deg,
      color-mix(in srgb, var(--surface) 96%, transparent),
      color-mix(in srgb, var(--bg) 88%, transparent));
  border: 1px solid color-mix(in srgb, var(--core-ink) 18%, var(--line));
  box-shadow:
    0 16px 34px -22px rgba(0, 0, 0, .55),
    inset 0 1px 0 color-mix(in srgb, #fff 7%, transparent);
}

/* A hairline of brand colour along the top edge. One detail, on four
   cards, rather than a gradient on each - which would compete with the
   figures the cards exist to show. */
.shell .stat::after {
  content: "";
  position: absolute;
  inset: 0 0 auto 0;
  height: 2px;
  background: linear-gradient(90deg,
    transparent, var(--core-ink), transparent);
  opacity: .5;
}

.shell .stat .stat-icon {
  display: grid;
  place-items: center;
  width: 42px;
  height: 42px;
  border-radius: 13px;
  background: color-mix(in srgb, var(--core-ink) 13%, transparent);
  border: 1px solid color-mix(in srgb, var(--core-ink) 26%, transparent);
  color: var(--core-ink);
}
.shell .stat .stat-icon svg { width: 20px; height: 20px; }
.shell .stat .stat-value { font-size: 2rem; letter-spacing: -.02em; }

/* ---- search ------------------------------------------------------------ */

.platform-search {
  position: relative;
  display: flex;
  align-items: center;
  margin: 0 0 18px;
}
.platform-search svg {
  position: absolute;
  left: 16px;
  width: 18px;
  height: 18px;
  color: var(--text-3);
  pointer-events: none;
}
.platform-search input {
  width: 100%;
  min-height: 50px;
  padding: 0 16px 0 46px;
  border-radius: 14px;
  border: 1px solid color-mix(in srgb, var(--core-ink) 16%, var(--line));
  background: color-mix(in srgb, var(--surface) 80%, transparent);
  color: var(--text-1);
  font-size: .95rem;
}
.platform-search input:focus {
  outline: none;
  border-color: var(--core-ink);
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--core-ink) 18%, transparent);
}

tr.filter-empty td {
  padding: 22px 14px;
  text-align: center;
  color: var(--text-3);
  font-size: .9rem;
}

/* ---- the empty state --------------------------------------------------- */

.platform-empty {
  position: relative;
  overflow: hidden;
  border-radius: 18px;
  padding: 54px 24px 58px;
  text-align: center;
  background:
    radial-gradient(60% 70% at 50% 40%,
      color-mix(in srgb, var(--core-ink) 12%, transparent), transparent 72%),
    color-mix(in srgb, var(--surface) 70%, transparent);
  border: 1px solid color-mix(in srgb, var(--core-ink) 18%, var(--line));
}

/* The flowing lines sit behind the message and are stretched to the panel
   with preserveAspectRatio="none". They carry no data - decoration only,
   and aria-hidden - so nothing here implies figures that do not exist. */
.platform-empty-art {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  pointer-events: none;
}
.platform-empty-art .flow {
  fill: none;
  stroke: var(--core-ink);
  stroke-width: 1.4;
  opacity: .3;
}
.platform-empty-art .flow.two { opacity: .18; }
.platform-empty-art .flow-node {
  fill: var(--core-ink);
  opacity: .8;
  animation: platform-node 4.5s ease-in-out infinite;
  animation-delay: calc(var(--i) * .5s);
}
@keyframes platform-node {
  0%, 100% { opacity: .45; }
  50%      { opacity: 1; }
}

.platform-empty-body { position: relative; }
.platform-empty-icon {
  display: grid;
  place-items: center;
  width: 78px;
  height: 78px;
  margin: 0 auto 20px;
  border-radius: 50%;
  background: color-mix(in srgb, var(--core-ink) 14%, transparent);
  border: 1px solid color-mix(in srgb, var(--core-ink) 32%, transparent);
  color: var(--core-ink);
  box-shadow: 0 0 34px -6px var(--core-glow);
}
.platform-empty-icon svg { width: 32px; height: 32px; }
.platform-empty h3 { font-size: 1.4rem; margin: 0 0 8px; }
.platform-empty p {
  max-width: 420px;
  margin: 0 auto 22px;
  color: var(--text-2);
  font-size: .94rem;
}

@media (prefers-reduced-motion: reduce) {
  .platform-empty-art .flow-node { animation: none; }
}

@media (max-width: 640px) {
  .platform-empty { padding: 38px 18px 42px; }
  .shell .stat .stat-value { font-size: 1.6rem; }
}

/* ==========================================================================
   Typography scale
   ==========================================================================

   One scale for the sign-in card, the secured page and every Super Admin
   surface, so the three read as one product. Declared as tokens rather than
   repeated numbers: the previous passes set sizes inline at each call site,
   which is how a page title ended up at four different sizes depending on
   which iteration last touched it.

   Sizes are in rem so they follow the browser's own text setting. Nothing
   here is larger than it was - the change is that the steps are now
   consistent and the small end has a floor. */

:root {
  --type-page: 1.72rem;      /* page and card titles - one dominant step   */
  --type-section: 1.06rem;   /* section headings inside a card             */
  --type-kpi: 2rem;          /* the figure on a KPI card                   */
  --type-body: .94rem;       /* paragraphs and table cells                 */
  --type-label: .82rem;      /* field labels, KPI labels                   */
  --type-helper: .8rem;      /* hints and supporting lines - the floor     */
  --line-tight: 1.25;        /* headings                                   */
  --line-body: 1.55;         /* anything read in sentences                 */
}

.shell .page-head h1,
.auth .signin-card h1 {
  font-size: var(--type-page);
  line-height: var(--line-tight);
  letter-spacing: -.02em;
  font-weight: 700;
}

.shell .card > header h2 {
  font-size: var(--type-section);
  line-height: var(--line-tight);
  font-weight: 650;
}

.shell .stat .stat-value { font-size: var(--type-kpi); }
.shell .stat .stat-label {
  font-size: var(--type-label);
  font-weight: 600;
  /* Not uppercase. Six words in small caps is a label; a whole interface of
     them is shouting, and it costs legibility at this size. */
  text-transform: none;
  letter-spacing: 0;
}
.shell .stat .stat-note {
  font-size: var(--type-helper);
  line-height: var(--line-body);
}

.shell .page-head p,
.shell .card > header p,
.auth .signin-card .sub {
  font-size: var(--type-body);
  line-height: var(--line-body);
}

/* The helper line under a field is the most-skipped text in any form and
   the easiest to make too faint. It gets the floor size and a colour that
   passes contrast, never an opacity. */
.shell .field .hint,
.auth .signin-card .field .hint {
  font-size: var(--type-helper);
  line-height: var(--line-body);
  color: var(--text-3);
  opacity: 1;
}

/* ==========================================================================
   Super Admin: shared surfaces
   ==========================================================================

   Styled on the shared primitives - card, table, page-head, empty, badge -
   rather than per page, so Packages, Subscriptions, Pricing, Backup,
   Settings and Audit all pick this up without being touched individually,
   and a new platform page is consistent the day it is written.

   Every rule stays behind `.shell.platform`. An academy admin, coach,
   parent or player must keep the calmer product styling. */

.shell .card {
  border-radius: 16px;
  border: 1px solid color-mix(in srgb, var(--core-ink) 14%, var(--line));
  background:
    linear-gradient(168deg,
      color-mix(in srgb, var(--surface) 96%, transparent),
      color-mix(in srgb, var(--surface) 88%, transparent));
  box-shadow: 0 14px 30px -24px rgba(0, 0, 0, .5);
}

.shell .card > header {
  border-bottom-color: color-mix(in srgb, var(--core-ink) 12%, var(--line));
}

/* Table rows get their separation from a hairline rather than banding.
   Zebra stripes on a dark surface read as two different backgrounds. */
.shell table tbody tr {
  border-bottom: 1px solid color-mix(in srgb, var(--core-ink) 8%, var(--line));
}
.shell table tbody tr:last-child { border-bottom: 0; }
.shell table tbody tr:hover {
  background: color-mix(in srgb, var(--core-ink) 6%, transparent);
}
.shell.platform table thead th {
  font-size: var(--type-helper);
  font-weight: 650;
  color: var(--text-2);
  letter-spacing: .02em;
}

/* The shared empty state, so a page with nothing in it still looks
   deliberate. The Academies page has its own richer panel; this is the
   baseline every other platform page gets. */
.shell.platform .empty {
  border-radius: 16px;
  background:
    radial-gradient(60% 70% at 50% 30%,
      color-mix(in srgb, var(--core-ink) 8%, transparent), transparent 70%);
  border: 1px dashed color-mix(in srgb, var(--core-ink) 22%, var(--line));
}
.shell.platform .empty h3 { font-size: var(--type-section); }
.shell.platform .empty p {
  font-size: var(--type-body);
  line-height: var(--line-body);
}

/* Micro-interaction only: a hairline lift on hover, no float, no scale. */
.shell .stat { transition: transform .18s ease, box-shadow .18s ease; }
.shell .stat:hover {
  transform: translateY(-2px);
  box-shadow:
    0 20px 38px -22px rgba(0, 0, 0, .6),
    inset 0 1px 0 color-mix(in srgb, #fff 8%, transparent);
}

@media (prefers-reduced-motion: reduce) {
  .shell .stat { transition: none; }
  .shell .stat:hover { transform: none; }
}

/* Below the sidebar breakpoint the KPI row stacks two-up rather than
   four-across, which at 360px would leave each figure in a 70px column. */
@media (max-width: 900px) {
  .shell.platform .stats.four { grid-template-columns: repeat(2, 1fr); }
}
@media (max-width: 520px) {
  .shell.platform .stats.four { grid-template-columns: 1fr; }
  .shell.platform .page-head h1 { font-size: 1.4rem; }
}

/* ---- theme and language, before sign-in --------------------------------- */

/* These sit on the authentication card rather than inside Settings, which a
   person cannot reach until they have an account, so somebody who needs the
   dark theme got a white flash on every visit before signing in. */

.prefs-bar {
  display: flex;
  align-items: center;
  gap: 6px;
  margin: 0 0 18px;
}

.prefs-btn {
  display: inline-flex;
  align-items: center;
  min-height: 32px;
  padding: 0 11px;
  border-radius: 8px;
  border: 1px solid var(--line);
  background: transparent;
  color: var(--text-2);
  font-size: .8rem;
  font-weight: 600;
  text-decoration: none;
  cursor: pointer;
  transition: background .15s ease, color .15s ease, border-color .15s ease;
}
.prefs-btn:hover {
  color: var(--text-1);
  border-color: var(--core-glass-edge);
  background: color-mix(in srgb, var(--core-ink) 7%, transparent);
}
/* The selected language, not merely hovered. aria-current says the same
   thing to a screen reader, which colour alone does not. */
.prefs-btn.on {
  color: var(--core-ink);
  border-color: var(--core-ink);
  background: color-mix(in srgb, var(--core-ink) 10%, transparent);
}
.prefs-btn:focus-visible {
  outline: 2px solid var(--core-ink);
  outline-offset: 2px;
}

.prefs-sep {
  width: 1px;
  height: 18px;
  margin: 0 4px;
  background: var(--line);
}

@media (max-width: 420px) {
  .prefs-bar { flex-wrap: wrap; }
}

/* ---- the welcome an academy sees once ----------------------------------- */

.welcome-wrap {
  display: grid;
  place-items: center;
  min-height: 60vh;
  padding: 24px 16px;
}
.welcome-card {
  max-width: 560px;
  text-align: center;
  border-radius: 20px;
}
.welcome-card .body { padding: 34px 30px 30px; }
.welcome-card .prefs-bar { justify-content: center; }
.welcome-mark img { height: 40px; width: auto; margin: 6px 0 20px; }
.welcome-card h1 {
  font-size: var(--type-page);
  line-height: var(--line-tight);
  margin-bottom: 10px;
}
.welcome-thanks {
  font-size: 1.02rem;
  font-weight: 600;
  color: var(--core-ink);
  margin-bottom: 14px;
}
.welcome-body {
  font-size: var(--type-body);
  line-height: var(--line-body);
  color: var(--text-2);
  margin: 0 auto 26px;
  max-width: 44ch;
}
@media (max-width: 480px) {
  .welcome-card .body { padding: 26px 20px 24px; }
  .welcome-card h1 { font-size: 1.4rem; }
}

/* ---- modals -------------------------------------------------------------

   A glass panel over the page, for a focused action. The drawer beside it
   is still right for a long form next to its list; this is for "onboard an
   academy", "record a payment", "are you sure".

   Glass is only readable if what shows through it is quiet. The scrim is
   doing as much work as the blur: without it the panel picks up whatever
   colour happens to be behind, and a form over a busy table becomes hard
   to read in exactly the moment somebody is typing into it. */

.modal {
  position: fixed;
  inset: 0;
  z-index: 60;
  display: none;
  align-items: center;
  justify-content: center;
  padding: 24px;
}
.modal.open { display: flex; }

.modal-scrim {
  position: absolute;
  inset: 0;
  background: var(--glass-scrim);
  /* The blur belongs on the scrim, not the panel: blurring the panel
     blurs its own contents in some engines. */
  -webkit-backdrop-filter: var(--glass-blur);
  backdrop-filter: var(--glass-blur);
  animation: modal-fade .18s ease-out both;
}

.modal-panel {
  position: relative;
  display: flex;
  flex-direction: column;
  width: 100%;
  max-width: 520px;
  /* Never taller than the viewport: the body scrolls, the header and the
     actions do not. A Save button that scrolls off the bottom is a form
     people think is broken. */
  max-height: min(86vh, 780px);
  border-radius: 18px;
  border: 1px solid var(--glass-border);
  background: var(--glass-surface);
  box-shadow:
    0 24px 60px -12px rgba(11, 17, 32, .38),
    0 2px 8px rgba(11, 17, 32, .10);
  animation: modal-rise .2s cubic-bezier(.2, .8, .3, 1) both;
  overflow: hidden;
}
:root[data-theme="dark"] .modal-panel {
  border-color: rgba(255, 255, 255, .12);
  background: color-mix(in srgb, var(--surface) 92%, transparent);
  box-shadow: 0 24px 60px -12px rgba(0, 0, 0, .6);
}

.modal-sm .modal-panel { max-width: 400px; }
.modal-lg .modal-panel { max-width: 760px; }
.modal-full .modal-panel { max-width: 1040px; }

.modal-head {
  display: flex;
  align-items: flex-start;
  gap: 12px;
  padding: 20px 22px 14px;
  border-bottom: 1px solid var(--line);
}
.modal-head h2 {
  font-size: var(--type-section, 1.12rem);
  line-height: var(--line-tight, 1.25);
  margin: 0;
}
.modal-blurb {
  margin: 6px 0 0;
  font-size: .86rem;
  line-height: 1.5;
  color: var(--text-2);
}
.modal-x {
  margin-left: auto;
  flex: 0 0 auto;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 32px;
  height: 32px;
  border-radius: 9px;
  border: 1px solid transparent;
  background: transparent;
  color: var(--text-2);
  cursor: pointer;
}
.modal-x:hover {
  color: var(--text-1);
  background: color-mix(in srgb, var(--core-ink) 8%, transparent);
}
.modal-x:focus-visible {
  outline: 2px solid var(--core-ink);
  outline-offset: 2px;
}

.modal-body {
  padding: 18px 22px;
  overflow-y: auto;
  -webkit-overflow-scrolling: touch;
}
.modal-foot {
  display: flex;
  gap: 10px;
  justify-content: flex-end;
  padding: 14px 22px 18px;
  border-top: 1px solid var(--line);
  background: color-mix(in srgb, var(--surface) 70%, transparent);
}

/* Filled by a failed submission. Hidden until there is something to say -
   an empty red box above a form reads as an error nobody can act on. */
.modal-error {
  margin: 0 22px;
  padding: 10px 12px;
  border-radius: 10px;
  border: 1px solid color-mix(in srgb, var(--bad) 40%, transparent);
  background: color-mix(in srgb, var(--bad) 10%, transparent);
  color: var(--bad-ink, var(--bad));
  font-size: .86rem;
}
.modal-error[hidden] { display: none; }

@keyframes modal-fade { from { opacity: 0; } to { opacity: 1; } }
@keyframes modal-rise {
  from { opacity: 0; transform: translateY(12px) scale(.985); }
  to   { opacity: 1; transform: none; }
}
@media (prefers-reduced-motion: reduce) {
  .modal-scrim, .modal-panel { animation: none; }
}

/* On a phone the panel goes to the bottom and fills the width. A centred
   card with margins on a 360px screen wastes the only space there is, and
   the actions end up further from the thumb than the close button. */
@media (max-width: 560px) {
  .modal { padding: 0; align-items: flex-end; }
  .modal-panel {
    max-width: none;
    max-height: 92vh;
    border-radius: 18px 18px 0 0;
    border-bottom: none;
    animation-name: modal-sheet;
  }
  .modal-head { padding: 16px 16px 12px; }
  .modal-body { padding: 14px 16px; }
  .modal-error { margin: 0 16px; }
  .modal-foot {
    padding: 12px 16px calc(14px + env(safe-area-inset-bottom, 0px));
  }
  /* Full width buttons, in reach, and big enough to hit. */
  .modal-foot .btn { flex: 1; min-height: 46px; }
}
@keyframes modal-sheet {
  from { opacity: 0; transform: translateY(24px); }
  to   { opacity: 1; transform: none; }
}


/* Phone field: a country selector beside the number.

   The selector is sized to its content rather than split evenly - country
   names are long, but the number is the field people are actually filling
   in and it should get the room. */
.phone-field .phone-row {
  display: flex;
  gap: 8px;
  align-items: stretch;
}
.phone-field .phone-row select {
  flex: 0 0 auto;
  max-width: 44%;
  min-width: 0;
}
.phone-field .phone-row input {
  flex: 1 1 auto;
  min-width: 0;
}
@media (max-width: 560px) {
  /* Stacked on a phone: side by side, the selector squeezes the number
     into four visible digits. */
  .phone-field .phone-row { flex-direction: column; }
  .phone-field .phone-row select { max-width: 100%; }
}


/* A suspended academy in the rail.

   Dimmed, not hidden - it is the one somebody needs to open to find out
   why. The tag carries the state in words as well as in colour, because a
   colour alone says nothing to a screen reader. */
/* The label gives way to the tag rather than wrapping. A two-line rail
   item next to single-line ones breaks the rhythm of the whole list, and
   the full name is already in the tooltip and on the page it opens. */
.sidebar nav a.nav-muted .rail-hide {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  min-width: 0;
}

.sidebar nav a.nav-muted { opacity: .62; }
.sidebar nav a.nav-muted:hover,
.sidebar nav a.nav-muted.active { opacity: 1; }
.sidebar nav a .nav-state {
  margin-left: auto;
  font-size: .62rem;
  font-weight: 700;
  letter-spacing: .06em;
  text-transform: uppercase;
  color: var(--warning);
  border: 1px solid color-mix(in srgb, var(--warning) 42%, transparent);
  border-radius: 20px;
  padding: 1px 7px;
  flex: none;
}
[data-rail="1"] .sidebar nav a .nav-state { display: none; }


/* The "Lost your phone?" disclosure on the authenticator screen. */
.mfa-help { margin: 10px 0 16px; }
.mfa-help > summary {
  cursor: pointer;
  font-size: .86rem;
  font-weight: 600;
  color: var(--brand-700);
  list-style: none;
}
.mfa-help > summary::-webkit-details-marker { display: none; }
.mfa-help > summary::after { content: " \203A"; }
.mfa-help[open] > summary::after { content: " \2304"; }
.mfa-help > summary:hover { text-decoration: underline; }
.mfa-help-body {
  margin-top: 8px;
  padding: 12px 14px;
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  background: var(--surface-2);
}
.mfa-help-body p { margin: 0 0 8px; }
.mfa-help-body p:last-child { margin-bottom: 0; }


/* Brand artwork on the rail, per theme.

   The logo files come in a light variant (for a dark rail) and a dark one.
   CSS cannot change an <img> src, so both are in the markup and the wrong
   one is hidden. Hiding rather than filtering keeps the supplied artwork
   exactly as drawn. */
.brandmark .logo.for-dark-rail,
.sidebar .foot .built-by img.for-dark-rail { display: none; }
.brandmark .logo.for-light-rail,
.sidebar .foot .built-by img.for-light-rail { display: block; }
:root[data-theme="dark"] .brandmark .logo.for-light-rail,
:root[data-theme="dark"] .sidebar .foot .built-by img.for-light-rail { display: none; }
:root[data-theme="dark"] .brandmark .logo.for-dark-rail,
:root[data-theme="dark"] .sidebar .foot .built-by img.for-dark-rail { display: block; }


/* ==========================================================================
   The sign-in stage
   ==========================================================================

   A photograph of a training session, the proposition written over it, and
   the sign-in card floating on the right. It replaces the hexagon-and-cards
   artwork, which drew the product's features as a diagram rather than
   showing the thing the product is for.

   The artwork is the supplied sportsyncs-academyos-bg image. The painted
   gradient stays underneath it: if the file is ever missing or an old
   browser understands neither format, the page is still composed rather
   than blank, because a background-image that does not resolve simply does
   not paint and the next layer does.

   Accents here are the product's emerald, not the cyan of the reference
   image - see the note in the handover. */

.auth.auth-stage {
  position: relative;
  /* The viewport itself does not scroll on the split layout.
  
     background-attachment: fixed held the photograph still, but the form
     column was still scrolling the whole document - the hero went with it.
     Now the stage is exactly one viewport tall and hides its own overflow,
     and the only thing that scrolls is the form column. The hero cannot
     move because there is nowhere for it to move to.
     
     dvh after vh: on a phone the browser chrome eats into vh, and the
     older unit is the fallback for anything that does not know dvh. */
  height: 100vh;
  height: 100dvh;
  min-height: 0;
  overflow: hidden;
  display: grid;
  grid-template-columns: 1.15fr .85fr;
  align-items: center;
  gap: 0;
  isolation: isolate;
  background:
    /* The supplied artwork. image-set offers WebP first and falls back to
       the JPEG on anything that does not understand it - a background that
       silently does not paint is the worst kind of missing. */
    image-set(url("../img/sportsyncs-academyos-bg.webp") type("image/webp"),
              url("../img/sportsyncs-academyos-bg.jpg") type("image/jpeg")),
    /* Night sky into floodlit turf, so the page is composed either way. */
    radial-gradient(120% 80% at 20% 12%,
      rgba(16, 185, 129, .10), transparent 60%),
    linear-gradient(180deg, #0A0F14 0%, #101A20 46%, #0C2018 78%, #09150F 100%);
  background-size: cover, auto, auto;
  background-position: center, center, center;
  background-repeat: no-repeat;
  /* The photograph stays put while a long form scrolls over it. Without
     this the setup form - five fields and their help text - dragged the
     stadium up the screen with it on any viewport shorter than about
     860px, which is most laptops. */
  background-attachment: fixed, fixed, fixed;
}

/* A scrim, so text stays readable whatever photograph is dropped in. A
   photo chosen for its mood is rarely chosen for its contrast. */
.auth.auth-stage::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: -1;
  background:
    /* Darker, and darkened top and bottom as well as across.
    
       The photograph is busiest through its middle band - floodlights,
       the glass building, the pitch markings - which is exactly where the
       strap line and the feature row sit. The previous scrim was built for
       the painted gradient that preceded the photograph and left those
       lines fighting it. */
    linear-gradient(180deg, rgba(4, 12, 18, .42) 0%,
                            rgba(4, 12, 18, .10) 38%,
                            rgba(4, 12, 18, .52) 100%),
    linear-gradient(90deg, rgba(4, 12, 18, .94) 0%,
                           rgba(4, 12, 18, .82) 46%,
                           rgba(4, 12, 18, .86) 100%);
}

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

.auth-stage .auth-hero {
  /* First column, though it comes second in the markup: the sign-in card
     stays first in the document so keyboard and screen-reader users reach
     the thing they came for before the sales copy. */
  grid-column: 1;
  grid-row: 1;
  position: relative;
  /* height 100% with min-height: 0, not min-height: 100vh. A grid item
     defaults to min-height: auto, which refuses to shrink below its
     content - the scroll container below would never be able to shrink
     either, and the page would grow instead of scrolling inside. */
  height: 100%;
  min-height: 0;
  padding: 56px 40px 32px 64px;
  display: flex;
  flex-direction: column;
  /* The body centres; the attribution sits on the floor.
  
     With justify-content: center the two were stacked together in the
     middle, so "Powered by ThinkSyncs" read as another line of the feature
     block rather than as the footer of the page. space-between plus auto
     margins on the body gives a centred hero and a footer where a footer
     belongs. */
  justify-content: space-between;
  gap: 28px;
  color: #F8FAFC;
}

.auth-stage .auth-hero-body { margin: auto 0; }

.auth-stage .auth-eyebrow {
  margin: 0 0 10px;
  /* 1.3rem, and set as "SportSyncs" rather than shouted into capitals.
  
     Tracking is now .01em, near enough to none. Letter-spacing adds its
     space after every letter including the last, so a tracked line sits
     visually left of the untracked headline beneath it even though both
     start at the same x - which is the misalignment that showed. At this
     size the word needs no tracking to read as a brand anyway. */
  font-size: 1.3rem;
  font-weight: 700;
  letter-spacing: .01em;
  color: var(--brand-400);
}

.auth-stage .auth-headline {
  margin: 0 0 12px;
  /* Pulled left, not pushed right.
  
     I had this backwards: a positive indent moved the headline further
     from the edge, which is the direction it was already wrong in. The
     cause is the glyph, not the tracking - a bold "A" carries more side
     bearing than the "S" above it, so the headline sits visibly inset
     while both boxes start at the same x. A negative indent of .06em -
     about 4px at the headline's full size, and scaling with it - puts the
     two on the same optical edge. */
  text-indent: -.06em;
  font-size: clamp(2.6rem, 5vw, 4.1rem);
  line-height: 1.02;
  letter-spacing: -.02em;
  font-weight: 800;
  color: #FFFFFF;
}
.auth-stage .auth-headline span { color: var(--brand-400); }

/* Setup carries an extra line explaining what the account is for. */
.auth-stage .auth-note {
  margin: -22px 0 32px;
  max-width: 540px;
  font-size: .95rem;
  line-height: 1.6;
  color: rgba(248, 250, 252, .70);
}

.auth-stage .auth-strap {
  margin: 0 0 34px;
  font-size: clamp(1rem, 1.6vw, 1.28rem);
  color: rgba(248, 250, 252, .82);
}

.auth-stage .auth-features {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-wrap: wrap;
  gap: 0;
  max-width: 680px;
}
.auth-stage .auth-features li {
  /* Four across, evenly. flex-basis 150px let the fourth wrap onto its own
     line and the row stopped reading as a set. */
  flex: 1 1 0;
  min-width: 0;
  padding: 0 18px;
  border-left: 1px solid rgba(248, 250, 252, .16);
  font-size: .9rem;
  line-height: 1.35;
  color: rgba(248, 250, 252, .88);
}
.auth-stage .auth-features li:first-child {
  padding-left: 0;
  border-left: 0;
}
.auth-stage .auth-features svg {
  display: block;
  width: 26px; height: 26px;
  margin-bottom: 12px;
  color: var(--brand-400);
}

.auth-stage .auth-vendor {
  display: flex;
  align-items: center;
  gap: 10px;
  font-size: .8rem;
  color: rgba(248, 250, 252, .62);
}
.auth-stage .auth-vendor img { height: 22px; width: auto; display: block; }

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

.auth-stage .form-side {
  grid-column: 2;
  grid-row: 1;
  /* The one scroll container on the page. */
  height: 100%;
  min-height: 0;
  overflow-y: auto;
  overflow-x: hidden;
  display: flex;
  /* flex-start, not center: a card taller than the window cannot be
     centred in it - the top goes above the viewport and no amount of
     scrolling brings it back. It sits from the top and the page scrolls. */
  align-items: center;
  justify-content: center;
  padding: 40px 56px 40px 24px;
  background: none;
}

/* Light theme: the card lightens, the hero does not.
   
   The card was pinned to a dark surface whatever the theme, so the toggle
   on these screens appeared to do nothing - it switched a variable nothing
   on the page was reading. The hero stays light-on-photograph in both
   themes because it sits on the photograph, which does not change. */
:root:not([data-theme="dark"]) .auth-stage .form-side .panel {
  /* One glass surface for every pre-auth card - sign-in, setup, reset, the
     authenticator steps, the recovery codes and signed-out. They had
     drifted: the signed-out card and the sign-in card were different
     whites against the same photograph. */
  background: rgba(255, 255, 255, .90);
  border-color: rgba(15, 23, 42, .10);
  color: var(--text);
  -webkit-backdrop-filter: var(--glass-blur);
  backdrop-filter: var(--glass-blur);
  box-shadow:
    0 1px 2px rgba(15, 23, 42, .06),
    0 24px 56px -18px rgba(15, 23, 42, .28),
    inset 0 1px 0 rgba(255, 255, 255, .80);
}

/* Anything the card draws on itself follows the card, not the dark stage:
   the "Sign in again" button, the notice panels, the rules between
   sections. These kept their dark-surface colours and looked pasted on. */
:root:not([data-theme="dark"]) .auth-stage .form-side .panel hr,
:root:not([data-theme="dark"]) .auth-stage .form-side .panel .rule {
  border-color: var(--line);
}
:root:not([data-theme="dark"]) .auth-stage .form-side .panel .note,
:root:not([data-theme="dark"]) .auth-stage .form-side .panel .callout {
  background: var(--surface-2);
  border-color: var(--line);
  color: var(--text-2);
}
:root:not([data-theme="dark"]) .auth-stage .form-side .panel a { color: var(--brand-700); }

/* The panel sets a dark text colour for light mode, and a filled button
   inside it inherited that - "Sign in again" came out dark grey on
   emerald. A filled button carries its own foreground wherever it sits. */
/* Filled brand buttons carry white text wherever they sit - but .sso is
   excluded, because those are not brand buttons.
   
   The Google button is white by Google's own rules, with near-black text.
   Without the exclusion this rule won on specificity and painted that
   label white on white: the button rendered as an empty white pill with
   only the G mark on it. */
.auth-stage .form-side .panel .btn:not(.ghost):not(.secondary):not(.sso),
:root:not([data-theme="dark"]) .auth-stage .form-side .panel .btn:not(.ghost):not(.secondary):not(.sso) {
  color: #FFFFFF;
}

/* Said again inside the card, so the sign-in providers keep their own
   colours whatever the card is doing. */
.auth-stage .form-side .panel .btn.sso.google {
  background: #FFFFFF;
  color: #1F1F1F;
  border-color: #DADCE0;
}
.auth-stage .form-side .panel .btn.sso:not(.google) {
  color: var(--text);
}
:root:not([data-theme="dark"]) .auth-stage .form-side .panel .btn.ghost,
:root:not([data-theme="dark"]) .auth-stage .form-side .panel .btn.secondary {
  color: var(--text);
  border-color: var(--line);
}
:root:not([data-theme="dark"]) .auth-stage .form-side .panel h1,
:root:not([data-theme="dark"]) .auth-stage .form-side .panel h2 { color: var(--text); }
:root:not([data-theme="dark"]) .auth-stage .form-side .panel .sub,
:root:not([data-theme="dark"]) .auth-stage .form-side .panel .hint,
:root:not([data-theme="dark"]) .auth-stage .form-side .panel .small { color: var(--text-2); }
:root:not([data-theme="dark"]) .auth-stage .form-side .panel .field > span:first-child { color: var(--text); }
:root:not([data-theme="dark"]) .auth-stage .form-side .panel input,
:root:not([data-theme="dark"]) .auth-stage .form-side .panel select {
  background: #FFFFFF;
  border-color: var(--line);
  color: var(--text);
}
:root:not([data-theme="dark"]) .auth-stage .form-side .panel input::placeholder { color: var(--text-3); }

.auth-stage .form-side .panel {
  width: 100%;
  max-width: 368px;
  /* margin: auto on a flex child centres it when there is room and stops
     centring when there is not, which is exactly the behaviour a long
     form needs. */
  margin: auto;
  padding: 30px 28px;
  border-radius: 20px;
  /* Darker than --glass-surface, which is built from --surface and is white
     in light mode. This card sits on a photograph in both themes. */
  background: rgba(10, 18, 22, .72);
  border: 1px solid rgba(248, 250, 252, .12);
  -webkit-backdrop-filter: var(--glass-blur);
  backdrop-filter: var(--glass-blur);
  box-shadow:
    0 1px 2px rgba(0, 0, 0, .34),
    0 24px 56px -18px rgba(0, 0, 0, .62),
    inset 0 1px 0 rgba(248, 250, 252, .08);
  color: #F8FAFC;
}

.auth-stage .form-side .panel h1,
.auth-stage .form-side .panel h2 { color: #FFFFFF; }
.auth-stage .form-side .panel .sub,
.auth-stage .form-side .panel .hint,
.auth-stage .form-side .panel .small { color: rgba(248, 250, 252, .70); }
.auth-stage .form-side .panel .field > span:first-child { color: rgba(248, 250, 252, .88); }
.auth-stage .form-side .panel input,
.auth-stage .form-side .panel select {
  background: rgba(248, 250, 252, .06);
  border-color: rgba(248, 250, 252, .18);
  color: #F8FAFC;
}
.auth-stage .form-side .panel input::placeholder { color: rgba(248, 250, 252, .42); }
.auth-stage .form-side .panel input:focus {
  border-color: var(--brand-400);
  background: rgba(248, 250, 252, .10);
}

/* ---- narrower screens --------------------------------------------------- */

@media (max-width: 1080px) {
  /* Stacked: hand scrolling back to the document.
  
     The desktop rules lock the viewport and scroll one column. Left in
     place here they would trap the whole stacked page inside a column-
     height box, giving a nested scrollbar on a phone and hiding the hero
     below it. Every desktop constraint is released explicitly rather than
     relying on a later rule happening to win. */
  .auth.auth-stage {
    grid-template-columns: minmax(0, 1fr);
    height: auto;
    min-height: 100vh;
    min-height: 100dvh;
    overflow: visible;
  }
  .auth-stage .auth-hero { height: auto; min-height: 0; }
  .auth-stage .form-side {
    height: auto;
    min-height: 0;
    overflow-y: visible;
    overflow-x: visible;
  }
  /* The card first: somebody arriving here wants to sign in, not read the
     proposition again. */
  .auth-stage .form-side {
    grid-column: 1; grid-row: 1;
    min-height: auto; padding: 32px 20px 8px;
  }
  .auth-stage .auth-hero {
    grid-column: 1; grid-row: 2;
    min-height: auto;
    padding: 8px 24px 32px;
    text-align: center;
    align-items: center;
  }
  .auth-stage .auth-features { justify-content: center; }
  .auth-stage .auth-features li {
    border-left: 0;
    flex: 0 1 46%;
    padding: 12px 8px;
  }
  .auth-stage .auth-vendor { justify-content: center; }
  /* Stacked, the hero is no longer full height, so there is no floor to
     sit on - the auto margins would otherwise open a gap. */
  .auth-stage .auth-hero-body { margin: 0; }
}

@media (max-width: 560px) {
  .auth-stage .form-side .panel { padding: 24px 20px; border-radius: 16px; }
  .auth-stage .auth-headline { font-size: 2.2rem; }
}

.auth-stage .auth-strap.is-secured { display: flex; align-items: center; gap: 10px; }
.auth-stage .auth-strap.is-secured svg { width: 22px; height: 22px; color: var(--brand-400); }


/* ==========================================================================
   The pre-auth card: one scale for all seven
   ==========================================================================

   Sign-in, setup, reset, both authenticator steps, the recovery codes and
   signed-out. They had each inherited a different mix of rules - three
   separate h1 sizes between them - so the same card looked slightly
   different depending on which screen you arrived at.

   Scoped to .auth-stage so it beats the older .auth rules without having
   to find and unpick them; those still serve any screen not on the stage. */

.auth-stage .form-side .panel {
  /* Tighter again. A setup form is five fields, three hints and a button,
     and on a 900px-tall laptop the card ran past the fold. The savings are
     spread across the rhythm - padding, the gap under each field, the
     label and hint gaps, the control heights - because taking it all from
     one of them would just make that one look wrong. */
  padding: 30px 28px;
}

/* The mark. Larger again: on the stage the card is wide and mostly glass,
   and the brand is the first thing that should register. */
.auth-stage .signin-brand { margin-bottom: 18px; }
.auth-stage .signin-brand img { height: 58px; width: auto; }

/* Heading and standfirst. */
.auth-stage .form-side .panel h1 {
  font-size: 1.72rem;
  line-height: 1.2;
  letter-spacing: -.015em;
  margin: 0 0 6px;
}
.auth-stage .form-side .panel .sub {
  font-size: 1rem;
  line-height: 1.55;
  margin: 0 0 20px;
}

/* Fields: one rhythm down the card, rather than each screen's own. */
.auth-stage .form-side .panel .field { margin-bottom: 13px; }
.auth-stage .form-side .panel .field > span:first-child {
  font-size: .92rem;
  font-weight: 600;
  margin-bottom: 5px;
  display: block;
}
.auth-stage .form-side .panel .field input,
.auth-stage .form-side .panel .field select {
  font-size: 1rem;
  padding: 10px 14px;
}

/* Put back the room the icon needs.
   
   .with-icon reserves 42px on the left for the glyph sitting inside the
   field. The rule above is later and more specific, so its shorthand
   padding replaced that inset and the placeholder printed on top of the
   icon - "e.g. ufa" over the building, "Enter your email" over the
   envelope. Restated here at matching specificity rather than loosening
   the rule above, so the two cannot disagree again. */
.auth-stage .form-side .panel .with-icon > input {
  padding-left: 42px;
}
.auth-stage .form-side .panel .with-reveal.with-icon > input {
  padding-left: 42px;
  padding-right: 44px;
}
.auth-stage .form-side .panel .with-reveal > input {
  padding-right: 44px;
}
.auth-stage .form-side .panel .hint {
  font-size: .86rem;
  line-height: 1.5;
  margin-top: 5px;
  display: block;
}

/* Actions and the small print under them. */
.auth-stage .form-side .panel .btn {
  font-size: 1rem;
  padding: 11px 18px;
}
.auth-stage .form-side .panel .small,
.auth-stage .form-side .panel .newcomer {
  font-size: .88rem;
  line-height: 1.55;
}

/* A narrow card keeps the rhythm but loses the generous padding. */
@media (max-width: 560px) {
  .auth-stage .form-side .panel { padding: 24px 20px; }
  .auth-stage .signin-brand img { height: 46px; }
  .auth-stage .form-side .panel h1 { font-size: 1.5rem; }
}


/* ==========================================================================
   The collapsed rail: branding
   ==========================================================================

   The per-theme logo swap sets display: block on .logo, and it is declared
   after [data-rail="1"] .rail-hide { display: none } at the same
   specificity - so the wide wordmark came back in the 72px rail and was
   cropped against its edge. Restated here, last and more specific, so the
   rail wins whatever the theme rules say. */

[data-rail="1"] .sidebar .brandmark .logo,
[data-rail="1"] .sidebar .brandmark .logo.for-light-rail,
[data-rail="1"] .sidebar .brandmark .logo.for-dark-rail {
  display: none;
}

/* The compact SportSyncs mark - square, so it fits the rail as drawn. */
[data-rail="1"] .sidebar .brandmark .mark {
  display: block;
  height: 34px;
  width: 34px;
  margin: 0 auto;
}

/* An uploaded platform logo in the rail: whatever shape it is, it fits.
   contain scales it down inside the box without cropping or stretching. */
.sidebar .brandmark .platform-mark { display: none; }
[data-rail="1"] .sidebar .brandmark .platform-mark {
  display: block;
  height: 34px;
  max-width: 100%;
  width: auto;
  object-fit: contain;
  margin: 0 auto;
}
/* Nothing in the brand area may exceed the rail. The wordmark overflowing
   was what made "Platform" run past the edge. */
[data-rail="1"] .sidebar .brand,
[data-rail="1"] .sidebar .brandmark {
  max-width: 100%;
  overflow: hidden;
}
[data-rail="1"] .sidebar .brandmark { justify-content: center; }


/* ==========================================================================
   The sidebar brand band
   ========================================================================== */

/* Larger, and the line beneath it centred under it.
   
   34px read as a label on the navigation rather than as the product the
   navigation belongs to. 44px gives it the weight of a masthead without
   growing the band, whose padding is unchanged. */
.sidebar .brandmark .logo { height: 44px; }

/* "Platform" sat left while the logo above it ran the width of the band,
   so the two did not look like one block. Centred under it. The academy
   shell keeps its left-aligned chip - there the tenant line is a label on
   a logo, not a subtitle under a wordmark. */
.shell.platform .sidebar .brandmark { justify-content: center; }
.shell.platform .sidebar .tenant { justify-content: center; }

/* The rail hides the tenant's name - again.
   
   .sidebar .tenant .names sets display: flex and is declared after
   [data-rail="1"] .rail-hide { display: none } at equal specificity, so
   later won and the academy's name came back inside a 72px rail: the
   clipped "Un... / UFA" beside the monogram. Restated last and more
   specific. The monogram stays; it is square and it is the point. */
[data-rail="1"] .sidebar .tenant .names,
[data-rail="1"] .sidebar .tenant .rail-hide {
  display: none;
}
[data-rail="1"] .sidebar .tenant {
  justify-content: center;
  margin-top: 12px;
  max-width: 100%;
  overflow: hidden;
}
[data-rail="1"] .sidebar .tenant .tenant-logo {
  margin: 0 auto;
}


/* Notched phones: keep the card and the hero out from under the hardware.
   
   viewport-fit=cover lets the page paint edge to edge, which is what the
   photograph wants - and then the notch and the home indicator sit on top
   of whatever is there. The insets are zero on every device without them,
   so this costs nothing anywhere else. */
@media (max-width: 1080px) {
  .auth.auth-stage {
    padding-top: env(safe-area-inset-top, 0px);
    padding-bottom: env(safe-area-inset-bottom, 0px);
  }
  .auth-stage .form-side {
    padding-left: max(20px, env(safe-area-inset-left, 0px));
    padding-right: max(20px, env(safe-area-inset-right, 0px));
  }
  .auth-stage .auth-hero {
    padding-left: max(24px, env(safe-area-inset-left, 0px));
    padding-right: max(24px, env(safe-area-inset-right, 0px));
  }
}

/* iOS ignores background-attachment: fixed. The desktop hero is held still
   by the layout rather than by that property, so nothing is lost there; on
   a stacked phone the background scrolls with the page, which is the
   normal behaviour anyway. Stated so the next person does not chase it. */
@supports (-webkit-touch-callout: none) {
  .auth.auth-stage { background-attachment: scroll, scroll, scroll; }
}
