/* KarateShowCase tenant — utilities
 *
 * Generated by tools/consolidate-tenant-css.mjs from 50 source files,
 * concatenated in their original cascade order. Edit the sources, re-run the
 * tool; do not hand-edit this file.
 */

/* ══════════════════════════════════════════════════════════════════════
   dojo-smart-announcement-premium-styles.v1.css
   ══════════════════════════════════════════════════════════════════════ */
    /* ── Smart Announcement — static premium styles (no animation/motion) ── */
    .cc-announcement.smart-announcement {
      --smart-accent: var(--ks-accent);
      --smart-accent-soft: color-mix(in oklab, var(--accent) 12%, transparent);
      --smart-glow: color-mix(in oklab, var(--accent) 8%, transparent);
      --smart-border: color-mix(in oklab, var(--accent) 25%, transparent);
      --smart-surface: linear-gradient(135deg, #0A1324 0%, #1A2942 100%);
      --smart-text: var(--ks-accent);
      background: var(--smart-surface);
      border: 1px solid var(--smart-border);
      box-shadow: 0 4px 16px var(--smart-glow), inset 0 1px 0 rgba(255, 255, 255, 0.04);
    }

    .cc-announcement.smart-announcement .cc-announcement-text {
      color: var(--smart-text);
      -webkit-text-fill-color: var(--smart-text);
    }

    .cc-announcement.smart-announcement .cc-announcement-label {
      color: var(--smart-accent);
    }

    /* Inside the team-highlight card the ordinary bar is stripped to plain
       stacked text (see dojo-tenant-surface-components.v1.css). An occasion
       look is the opposite of that — it is a painted banner — so it takes the
       full column width and centres, which is how the same look reads on every
       other surface it appears on. Specificity, not order: these are (0,3,0)
       against the card's (0,2,0), so they hold regardless of bundle position. */
    .ks-highlight-body .cc-announcement.smart-announcement {
      width: 100%;
      align-items: center;
      text-align: center;
    }

    .ks-highlight-body .cc-announcement.smart-announcement .cc-announcement-text {
      text-align: center;
    }

    /* Mode: standard — clean platinum edge */
    .cc-announcement.smart-announcement-standard {
      --smart-accent: #D4D4D8;
      --smart-accent-soft: rgba(212, 212, 216, 0.08);
      --smart-glow: rgba(212, 212, 216, 0.06);
      --smart-border: rgba(212, 212, 216, 0.18);
      --smart-surface: linear-gradient(135deg, #0F1218 0%, #1A1D26 100%);
      --smart-text: #E4E4E7;
    }

    /* Mode: tournament-countdown — navy/gold championship */
    .cc-announcement.smart-announcement-tournament-countdown {
      --smart-accent: #F59E0B;
      --smart-accent-soft: rgba(245, 158, 11, 0.1);
      --smart-glow: rgba(245, 158, 11, 0.1);
      --smart-border: rgba(245, 158, 11, 0.25);
      --smart-surface: linear-gradient(135deg, #0F172A 0%, #1E293B 100%);
      --smart-text: #FCD34D;
      /* Stack vertically for clean 3-line layout */
      flex-direction: column;
      gap: 0.35rem;
      padding: 1.2rem 1.5rem;
    }

    /* Kicker line — compact label above everything */
    .cc-ann-kicker {
      font-family: var(--ks-display);
      font-size: .65rem;
      font-weight: 800;
      letter-spacing: 2.5px;
      text-transform: uppercase;
      color: var(--smart-accent, #F59E0B);
      opacity: 0.85;
    }

    /* Meta/countdown line — smaller than title */
    .cc-ann-meta {
      font-size: .82rem;
      font-weight: 700;
      letter-spacing: 1px;
      color: rgba(255, 255, 255, 0.55);
      text-transform: uppercase;
    }

    /* Title stays the strongest visual element */
    .cc-announcement.smart-announcement-tournament-countdown .cc-announcement-text {
      font-size: 1.3rem;
      font-weight: 900;
      letter-spacing: .5px;
      line-height: 1.35;
    }

    /* Hide the old .cc-announcement-label in tournament-countdown (replaced by kicker+meta) */
    .cc-announcement.smart-announcement-tournament-countdown .cc-announcement-label {
      display: none;
    }

    @media (max-width: 640px) {
      .cc-announcement.smart-announcement-tournament-countdown .cc-announcement-text {
        font-size: 1.1rem;
      }

      .cc-ann-kicker {
        font-size: .58rem;
        letter-spacing: 2px;
      }

      .cc-ann-meta {
        font-size: .75rem;
      }
    }

    /* Mode: birthday-spotlight — warm champagne */
    .cc-announcement.smart-announcement-birthday-spotlight {
      --smart-accent: #EC4899;
      --smart-accent-soft: rgba(236, 72, 153, 0.08);
      --smart-glow: rgba(236, 72, 153, 0.08);
      --smart-border: rgba(236, 72, 153, 0.2);
      --smart-surface: linear-gradient(135deg, #1A0A1E 0%, #2D1B30 100%);
      --smart-text: #F9A8D4;
    }

    /* Mode: champion-callout — victory gold finish */
    .cc-announcement.smart-announcement-champion-callout {
      --smart-accent: #FBBF24;
      --smart-accent-soft: rgba(251, 191, 36, 0.1);
      --smart-glow: rgba(251, 191, 36, 0.12);
      --smart-border: rgba(251, 191, 36, 0.3);
      --smart-surface: linear-gradient(135deg, #1A1600 0%, #2D2800 100%);
      --smart-text: #FDE68A;
    }

    /* Mode: urgent-update — clean amber/red alert */
    .cc-announcement.smart-announcement-urgent-update {
      --smart-accent: #EF4444;
      --smart-accent-soft: rgba(239, 68, 68, 0.08);
      --smart-glow: rgba(239, 68, 68, 0.1);
      --smart-border: rgba(239, 68, 68, 0.25);
      --smart-surface: linear-gradient(135deg, #1A0A0A 0%, #2D1515 100%);
      --smart-text: #FCA5A5;
    }

    /* Mode: registration-deadline — gold/amber deadline */
    .cc-announcement.smart-announcement-registration-deadline {
      --smart-accent: #F59E0B;
      --smart-accent-soft: rgba(245, 158, 11, 0.08);
      --smart-glow: rgba(245, 158, 11, 0.08);
      --smart-border: rgba(245, 158, 11, 0.22);
      --smart-surface: linear-gradient(135deg, #0A1520 0%, #152535 100%);
      --smart-text: #FCD34D;
    }

    /* Mode: holiday-premium — frosted platinum seasonal */
    .cc-announcement.smart-announcement-holiday-premium {
      --smart-accent: #10B981;
      --smart-accent-soft: rgba(16, 185, 129, 0.08);
      --smart-glow: rgba(16, 185, 129, 0.08);
      --smart-border: rgba(16, 185, 129, 0.2);
      --smart-surface: linear-gradient(135deg, #0A1A12 0%, #15302A 100%);
      --smart-text: #6EE7B7;
    }

    /* Mode: platinum-feature — best luxury platinum */
    .cc-announcement.smart-announcement-platinum-feature {
      --smart-accent: #A78BFA;
      --smart-accent-soft: rgba(167, 139, 250, 0.1);
      --smart-glow: rgba(167, 139, 250, 0.1);
      --smart-border: rgba(167, 139, 250, 0.22);
      --smart-surface: linear-gradient(135deg, #12101E 0%, #1E1B30 100%);
      --smart-text: #C4B5FD;
    }

    /* Intensity: subtle */
    .cc-announcement.smart-intensity-subtle {
      box-shadow: 0 2px 8px var(--smart-glow), inset 0 1px 0 rgba(255, 255, 255, 0.02);
    }

    /* Intensity: refined */
    .cc-announcement.smart-intensity-refined {
      box-shadow: 0 4px 20px var(--smart-glow), inset 0 1px 0 rgba(255, 255, 255, 0.05);
      border-width: 1.5px;
    }

    /* Intensity: spotlight */
    .cc-announcement.smart-intensity-spotlight {
      box-shadow: 0 6px 28px var(--smart-glow), 0 0 0 1px var(--smart-border), inset 0 1px 0 rgba(255, 255, 255, 0.07);
      border-width: 1.5px;
    }

    .smart-text-white {
      --smart-text-override: #ffffff;
    }

    .smart-text-navy {
      --smart-text-override: #0A1324;
    }

    .smart-text-black {
      --smart-text-override: #111111;
    }

    .smart-text-gold {
      --smart-text-override: var(--ks-accent);
    }

    .smart-text-platinum {
      --smart-text-override: #F7F4EA;
    }

    .smart-text-white .cc-announcement-text,
    .smart-text-white .cc-announcement-label,
    .smart-announcement.smart-text-white .home-special-title,
    .smart-announcement.smart-text-white .home-special-meta,
    .smart-announcement.smart-text-white .home-special-kicker,
    .smart-announcement.smart-text-white .home-special-list-item,
    .smart-text-navy .cc-announcement-text,
    .smart-text-navy .cc-announcement-label,
    .smart-announcement.smart-text-navy .home-special-title,
    .smart-announcement.smart-text-navy .home-special-meta,
    .smart-announcement.smart-text-navy .home-special-kicker,
    .smart-announcement.smart-text-navy .home-special-list-item,
    .smart-text-black .cc-announcement-text,
    .smart-text-black .cc-announcement-label,
    .smart-announcement.smart-text-black .home-special-title,
    .smart-announcement.smart-text-black .home-special-meta,
    .smart-announcement.smart-text-black .home-special-kicker,
    .smart-announcement.smart-text-black .home-special-list-item,
    .smart-text-gold .cc-announcement-text,
    .smart-text-gold .cc-announcement-label,
    .smart-announcement.smart-text-gold .home-special-title,
    .smart-announcement.smart-text-gold .home-special-meta,
    .smart-announcement.smart-text-gold .home-special-kicker,
    .smart-announcement.smart-text-gold .home-special-list-item,
    .smart-text-platinum .cc-announcement-text,
    .smart-text-platinum .cc-announcement-label,
    .smart-announcement.smart-text-platinum .home-special-title,
    .smart-announcement.smart-text-platinum .home-special-meta,
    .smart-announcement.smart-text-platinum .home-special-kicker,
    .smart-announcement.smart-text-platinum .home-special-list-item {
      color: var(--smart-text-override) !important;
      -webkit-text-fill-color: var(--smart-text-override) !important;
    }

    /* The preview's honesty line: with the master Visible toggle off, nothing
       the preview shows is live — smart mode included — and the preview says
       so in warning colour rather than impersonating a published banner. */
    #smart-ann-preview {
      flex-wrap: wrap;
    }

    #smart-ann-preview .smart-ann-visibility-state {
      flex-basis: 100%;
      margin: .45rem 0 0;
      font-size: .62rem;
      font-weight: 800;
      letter-spacing: .04em;
      text-align: center;
      color: #F5B85C;
    }


/* ══════════════════════════════════════════════════════════════════════
   dojo-home-command-center-responsive.v1.css
   ══════════════════════════════════════════════════════════════════════ */
    /* ── Command Center Responsive ────────────────────────────────────────── */
    @media (max-width: 900px) {
      .cc-top-row {
        grid-template-columns: 1fr 1fr;
      }

      .cc-quick-links-desktop {
        display: none;
      }

      .cc-quick-links-mobile {
        display: block;
        order: 2;
      }

      .cc-events-section {
        order: 1;
      }

      .cc-events-row {
        grid-template-columns: 1fr 1fr;
      }

      .cc-event-card:last-child:nth-child(odd) {
        grid-column: 1 / -1;
        max-width: 50%;
        justify-self: center;
      }

      .cc-announcement {
        justify-content: center;
        text-align: center;
      }

      .cc-announcement-link {
        align-self: center;
      }
    }

    @media (max-width: 640px) {
      .cc-top-row {
        grid-template-columns: 1fr;
      }

      .cc-quick-links-desktop {
        display: none;
      }

      .cc-quick-links-mobile {
        display: block;
      }

      .cc-events-row {
        grid-template-columns: 1fr;
      }

      .cc-event-card:last-child:nth-child(odd) {
        grid-column: auto;
        max-width: 100%;
        justify-self: auto;
      }

      .cc-announcement {
        flex-direction: column;
        align-items: center;
        gap: .6rem;
        text-align: center;
        padding: 1rem 1.2rem;
      }

      .cc-announcement-text {
        font-size: 1.05rem;
      }

      .cc-announcement-link {
        align-self: center;
      }

      .cc-quick-link {
        justify-content: center;
        text-align: center;
      }

      .cc-quick-link-label {
        flex: none;
      }

      .cc-card {
        padding: 1rem;
      }
    }


/* ══════════════════════════════════════════════════════════════════════
   dojo-home-command-center-button-polish.v1.css
   ══════════════════════════════════════════════════════════════════════ */
    /* ── Button shimmer sweep every 8s ──────────────────────────────────── */
    .cc-btn-gold {
      position: relative;
      overflow: hidden;
      transition: transform .2s cubic-bezier(.4, 0, .2, 1), box-shadow .2s ease, filter .2s ease;
    }


    .cc-btn-gold:hover {
      transform: translateY(-2px) scale(1.02);
      box-shadow: 0 4px 16px color-mix(in oklab, var(--accent) 40%, transparent);
      filter: brightness(1.06);
    }

    .cc-btn-gold:active {
      transform: translateY(0) scale(0.97);
      box-shadow: 0 1px 4px color-mix(in oklab, var(--accent) 25%, transparent);
      transition-duration: .08s;
    }

    .cc-btn-outline {
      position: relative;
      overflow: hidden;
      transition: transform .2s cubic-bezier(.4, 0, .2, 1), border-color .25s ease, color .25s ease, background .25s ease;
    }

    .cc-btn-outline:hover {
      transform: translateY(-1px);
    }

    .cc-btn-outline:active {
      transform: translateY(0) scale(0.97);
      transition-duration: .08s;
    }


/* ══════════════════════════════════════════════════════════════════════
   dojo-unified-premium-footer.v1.css
   ══════════════════════════════════════════════════════════════════════ */
    /* ── UNIFIED PREMIUM FOOTER ─────────────────────────────────────────────── */
    .site-footer {
      background: linear-gradient(180deg, #0A1324 0%, #0D1829 100%);
      border-top: 2px solid color-mix(in oklab, var(--accent) 35%, transparent);
      color: rgba(255, 255, 255, 0.7);
      font-family: 'Raleway', sans-serif;
      padding: 2.5rem 2rem 1.5rem;
      margin-top: 0;
    }

    .sf-inner {
      max-width: 1100px;
      margin: 0 auto;
    }

    /* Identity · the dojo's pages · the platform's pages · the message form.
       The form is the widest column because it holds inputs; identity comes
       next; the two nav lists take what a list needs. */
    .sf-top {
      display: grid;
      grid-template-columns: 1.15fr .6fr .6fr 1.25fr;
      gap: 2.5rem;
      padding-bottom: 1.5rem;
      border-bottom: 1px solid rgba(255, 255, 255, 0.08);
    }

    /* The dojo's own mark. Empty/error states render nothing — the identity
       column starts at the name instead of holding a box open. */
    .sf-brand-logo {
      display: block;
      max-height: 64px;
      width: auto;
      max-width: 160px;
      margin-bottom: .8rem;
      object-fit: contain;
    }

    .sf-brand-logo[data-media-state="empty"],
    .sf-brand-logo[data-media-state="error"] {
      display: none;
    }

    .sf-nav {
      display: flex;
      flex-direction: column;
      align-content: start;
      gap: .1rem;
    }

    .sf-nav-title {
      font-size: .66rem;
      font-weight: 800;
      letter-spacing: 1.6px;
      text-transform: uppercase;
      color: rgba(255, 255, 255, 0.45);
      margin: 0 0 .5rem;
    }

    /* Wide screens render the form directly; the disclosure toggle exists for
       the stacked footer alone (see the 768px block). */
    .sf-contact-form-mount .ks-contact-toggle {
      display: none;
    }

    .sf-contact-icon {
      display: inline-flex;
      flex-shrink: 0;
      color: var(--ks-accent-strong);
    }

    .sf-contact-icon .ks-icon {
      width: 14px;
      height: 14px;
    }

    .sf-brand h4 {
      font-family: var(--ks-display);
      font-size: 1.05rem;
      font-weight: 800;
      color: #FFFFFF;
      letter-spacing: 2px;
      text-transform: uppercase;
      margin: 0 0 .4rem;
    }

    .sf-brand p {
      font-size: .82rem;
      color: rgba(255, 255, 255, 0.5);
      margin: 0;
      line-height: 1.6;
    }

    .sf-brand a {
      color: var(--ks-accent-strong);
      text-decoration: none;
      font-weight: 700;
      transition: color .2s ease;
    }

    .sf-brand a:hover {
      color: var(--ks-accent-strong);
    }

    /* Natural case at readable size — a footer is navigation, not a
       specimen sheet of small caps (typography audit, 2026-08-15). The group
       labels above these keep their compact caps as semantic markers. */
    .sf-link {
      font-size: .82rem;
      font-weight: 600;
      letter-spacing: .01em;
      text-transform: none;
      color: rgba(255, 255, 255, 0.6);
      text-decoration: none;
      padding: .3rem 0;
      transition: color .2s ease, transform .15s ease;
      display: block;
      cursor: pointer;
    }

    .sf-link:hover {
      color: var(--ks-accent-strong);
      transform: translateX(3px);
    }

    .sf-bottom {
      display: flex;
      flex-wrap: wrap;
      justify-content: space-between;
      align-items: center;
      padding-top: 1.2rem;
      gap: .8rem;
    }

    /* 0.60 and 0.52, up from 0.40 and 0.30. On the footer band those measured
       3.81:1 and 2.67:1: the copyright and the trust line are quiet by intent,
       and quiet is not the same as unreadable. The same two lines are declared
       in four stylesheets -- this one and the three standalone page shells --
       so all four moved together. The one that gets missed is the one a reader
       lands on. */
    .sf-copy {
      font-size: .66rem;
      color: rgba(255, 255, 255, 0.6);
      letter-spacing: 1.5px;
      text-transform: uppercase;
    }

    .sf-trust {
      font-size: .58rem;
      color: rgba(255, 255, 255, 0.52);
      letter-spacing: 1px;
    }

    /* The platform's line, in the utility strip where attribution belongs —
       the identity column above it is the dojo's. */
    .sf-powered {
      font-size: .66rem;
      color: rgba(255, 255, 255, 0.6);
      letter-spacing: 1px;
    }

    .sf-powered a {
      color: var(--ks-accent-strong);
      text-decoration: none;
      font-weight: 700;
    }

    /* Footer responsive: four regions → two columns (identity+form wide) →
       one column, centred. */
    @media (max-width: 1080px) {
      .sf-top {
        grid-template-columns: 1fr 1fr;
        gap: 2rem;
      }
    }

    @media (max-width: 768px) {
      /* The stacked footer is composed, not piled: identity centred on top,
         then the two link groups SIDE BY SIDE as left-aligned columns — a
         single centred stack of every link read as a list of fourteen equal
         things. The groups' own labels carry the hierarchy. */
      .sf-top {
        grid-template-columns: 1fr 1fr;
        gap: 1.75rem 1.5rem;
      }

      .sf-brand {
        grid-column: 1 / -1;
        text-align: center;
      }

      .sf-brand-logo {
        margin-left: auto;
        margin-right: auto;
      }

      .sf-contact li {
        justify-content: center;
      }

      .sf-nav {
        align-items: flex-start;
        text-align: left;
        /* The columns sit against the centre line, not the screen edges. */
        justify-self: center;
        min-width: 9.5rem;
      }

      .sf-contact-form-mount {
        grid-column: 1 / -1;
        justify-self: center;
      }

      .sf-bottom {
        flex-direction: column;
        text-align: center;
      }

      /* Stacked, the form joins the centred rhythm of the columns above it:
         a constrained block on the centre line, heading centred, labels
         staying left on their own inputs the way form labels do. */
      .sf-contact-form-mount {
        width: 100%;
        max-width: 360px;
        margin: 0 auto;
      }

      .sf-contact-form-mount .ks-contact-heading {
        text-align: center;
      }

      /* The stacked footer centres itself, but a label belongs to its input,
         not to the centre line. */
      .sf-contact-form-mount .ks-contact-label {
        text-align: left;
      }

      /* Small screens get the intent as one line; the form unfolds on demand.
         The collapse is the grid-rows trick — pure CSS, contents intact — and
         the transition is transform-free and honoured only where motion is
         welcome. The toggle replaces the form's own heading here, so opening
         it does not say the same words twice. */
      .sf-contact-form-mount .ks-contact-toggle {
        display: inline-flex;
        align-items: center;
        justify-content: center;
        width: 100%;
        max-width: 360px;
        padding: .7rem 1rem;
        background: transparent;
        border: 1px solid rgba(255, 255, 255, 0.28);
        border-radius: var(--radius-full);
        color: rgba(255, 255, 255, 0.85);
        font-size: .72rem;
        font-weight: 700;
        letter-spacing: .08em;
        text-transform: uppercase;
        cursor: pointer;
        transition: border-color var(--duration-fast) var(--ease-out), color var(--duration-fast) var(--ease-out);
      }

      .sf-contact-form-mount .ks-contact-toggle:hover,
      .sf-contact-form-mount .ks-contact-toggle:focus-visible {
        border-color: var(--ks-accent-strong);
        color: #fff;
      }

      .sf-contact-form-mount .ks-contact-toggle:focus-visible {
        outline: 2px solid var(--ks-focus-ring);
        outline-offset: 2px;
      }

      /* height 0 → auto, animated where the browser can interpolate to a
         keyword (interpolate-size) and an instant, correct open everywhere
         else. The grid 0fr trick measured a 36px leak here: the form's own
         row gaps survive the crush in Chromium. */
      .sf-contact-form-mount .ks-contact-body {
        height: 0;
        overflow: hidden;
        interpolate-size: allow-keywords;
        transition: height .28s var(--ease-out);
      }

      .sf-contact-form-mount.is-open .ks-contact-body {
        height: auto;
      }

      .sf-contact-form-mount .ks-contact-heading {
        display: none;
      }

      .sf-contact-form-mount.is-open .ks-contact-body .ks-contact-form {
        padding-top: 1rem;
      }

      @media (prefers-reduced-motion: reduce) {
        .sf-contact-form-mount .ks-contact-body {
          transition: none;
        }
      }

      .site-footer {
        padding-bottom: 80px;
        /* clear mobile dock */
      }
    }


    /* ── The signed-in footer ───────────────────────────────────────────────
       A member inside the product does not need the marketing block -- the
       address, hours and contact belong to the public pages that court
       strangers. Signed in, the footer collapses to the utility strip: tenant
       name, legal links, the platform credit. One footer, two contexts, keyed
       off body.ks-signed-in rather than duplicated markup.

       The links a member still needs from a footer survive inside .sf-bottom;
       the .sf-top marketing grid is the part that goes. */
    body.ks-signed-in .site-footer {
      padding: 1.1rem 2rem;
    }

    body.ks-signed-in .site-footer .sf-top {
      display: none;
    }

    body.ks-signed-in .site-footer .sf-bottom {
      padding-top: 0;
      border-top: 0;
    }


/* ══════════════════════════════════════════════════════════════════════
   dojo-breadcrumb-back-link.v1.css
   ══════════════════════════════════════════════════════════════════════ */
    /* ── BREADCRUMB ──────────────────────────────────────────────────────────── */
    .breadcrumb {
      display: flex;
      align-items: center;
      gap: .5rem;
      padding: .8rem 1.5rem;
      font-size: .65rem;
      color: var(--muted);
      letter-spacing: 1px;
      background: var(--bg);
      border-bottom: var(--border-dim);
      flex-wrap: wrap
    }

    .breadcrumb a {
      color: var(--muted);
      cursor: pointer;
      transition: color .2s;
      text-decoration: none
    }

    .breadcrumb a:hover {
      color: var(--ks-accent-strong)
    }

    .bc-sep {
      color: var(--dim)
    }

    .bc-cur {
      color: var(--white);
      font-weight: 600
    }

    .back-link {
      display: inline-flex;
      align-items: center;
      gap: .5rem;
      padding: .9rem 1.5rem;
      font-size: .65rem;
      letter-spacing: 1.5px;
      text-transform: uppercase;
      color: var(--muted);
      cursor: pointer;
      border-bottom: var(--border-dim);
      background: var(--bg);
      transition: color .2s;
      font-weight: 700;
    }

    .back-link:hover {
      color: var(--ks-accent-strong)
    }


/* ══════════════════════════════════════════════════════════════════════
   dojo-homann-team-hub.v1.css
   ══════════════════════════════════════════════════════════════════════ */
    /* ══════════════════════════════════════════════════════════════════════════
       TEAM HUB — the team's own screen.

       V1's composition is the foundation and is deliberately recognisable: a
       dark tenant band, a stat strip straddling its foot, a wide operational
       column beside a member rail, collapsed tournament rows. What changes is
       the execution -- proportion, type, colour depth, imagery, density, and
       the order things arrive in.

       Five ideas are ported from V3 and nothing else: identity in the upper
       experience rather than a standalone card, a full-width announcement
       banner, the role read from the authorisation model, belt name plus a
       colour ring, and the compact signed-in footer. V3's white editorial page,
       sparse rhythm and thin rails are not here.

       Scoped to #view-tournament-hub. Tokens on the view, so nothing leaks.
       Colour is derived from the tenant's own band tokens; no hex belonging to
       a dojo appears in this file.
       ══════════════════════════════════════════════════════════════════════════ */

    /* .sr-hub is the product tour's mount for the same components (the
       next-tournament card drawn by ksHubUiNext over fixture data). It joins
       the scope rather than restating the tokens, so the tour's card can
       never drift from the hub's own colour. */
    /* #view-my-dashboard joins the scope for the same reason the tour's mount does:
       the student's hub is the same family as the team's, and one token set
       is what keeps the two from drifting apart by a shade. */
    /* dialog.ks-vd is the dashboard's video detail and its share form: they
       open in the top layer, outside every view, and take the same tokens so
       a clip looks the same over the dashboard as it does on it. */
    /* THE HUB'S DIALECT IS NOW A SET OF ALIASES. Every --ksh-* name below
       resolves to the foundation token the rest of the product reads
       (dojo-global-theme-tokens.v1.css), so the Team Hub, My Dashboard and
       the rest of the authenticated surface cannot drift apart by a shade —
       and a dojo's accent reaches here the way it reaches everywhere else.
       The names stay because this file's rules and the hub's markup spell
       them; the values are no longer this file's to choose.

       --ksh-gold stays what it was: --dojo-accent, which the branding runtime
       sets to the tenant's own accentColor and which defaults to the accepted
       gold where a dojo has chosen none. The hub's identity is the tenant's
       colour; this pass normalises what is underneath it, not the colour.
       --ksh-gold-ink was a literal dark gold; it is now that same colour
       darkened for a light surface, so it follows the accent too. */
    #view-tournament-hub,
    #view-my-dashboard,
    #view-training,
    .sr-hub,
    dialog.ks-sheet {
      --ksh-deep: var(--ks-band-deep);
      --ksh-mid: var(--ks-band);
      --ksh-raised: var(--ks-band-raised);
      --ksh-gold: var(--dojo-accent, var(--ks-accent));
      --ksh-gold-ink: color-mix(in oklab, var(--dojo-accent, var(--ks-accent)) 58%, #000);
      /* One metallic ramp, used in four restrained places: the hairline over
         the statistics, the gold action, the selected RSVP answer, and the
         band's foot. Anywhere else it stops reading as precious. */
      --ksh-gold-metal: linear-gradient(100deg, #8A6B1C 0%, #D6B95F 20%, #F4E6AE 38%, var(--ksh-gold) 56%, #8A6B1C 100%);
      --ksh-red: var(--primary);

      --ksh-surface: var(--ks-surface);
      --ksh-canvas: var(--ks-canvas);
      --ksh-sunken: var(--ks-surface-sunken);
      --ksh-line: var(--ks-line);
      --ksh-line-soft: var(--ks-line-soft);

      --ksh-ink: var(--ks-ink);
      --ksh-ink-2: var(--ks-ink-2);
      --ksh-ink-3: var(--ks-ink-3);
      --ksh-on-dark: var(--ks-band-ink);
      --ksh-on-dark-2: var(--ks-band-ink-2);
      --ksh-on-dark-3: var(--ks-band-ink-3);

      --ksh-r: var(--ks-r-lg);
      --ksh-r-sm: var(--ks-r-sm);
      --ksh-shadow: var(--ks-shadow-1);
      --ksh-shadow-lift: var(--ks-shadow-2);

      background: var(--ksh-canvas);
      color: var(--ksh-ink);
      font-family: 'Raleway', sans-serif;
      /* So the disclosure can transition to its own height. Ignored where
         unsupported, which simply means the panel opens instantly. */
      interpolate-size: allow-keywords;
    }

    /* ══ BAND ═══════════════════════════════════════════════════════════════════
       V1's dark opener, given depth: a key light from above, the tenant's hue
       running deep-to-raised beneath it, a floor vignette, and the dojo's own
       photograph behind all of it at low opacity with a reading scrim. No
       diagonal stripes -- the energy is light, not graphics.
       ═══════════════════════════════════════════════════════════════════════════ */
    .ksh-band {
      position: relative;
      isolation: isolate;
      overflow: hidden;
      padding-top: var(--ks-nav-h, 72px);
      background:
        radial-gradient(120% 78% at 50% -14%, rgba(255, 255, 255, .15) 0%, transparent 56%),
        radial-gradient(120% 90% at 50% 116%, rgba(0, 0, 0, .5) 0%, transparent 58%),
        linear-gradient(162deg, var(--ksh-deep) 0%, var(--ksh-mid) 50%, var(--ksh-raised) 100%);
      color: var(--ksh-on-dark);
    }

    .ksh-band-photo {
      position: absolute;
      inset: 0;
      z-index: -2;
    }

    .ksh-band-photo img {
      width: 100%;
      height: 100%;
      object-fit: cover;
      /* Faces sit high in a team photograph, so the crop holds the upper third. */
      object-position: center 28%;
      opacity: .3;
    }

    .ksh-band-photo img[data-media-state="empty"],
    .ksh-band-photo img[data-media-state="error"] {
      display: none;
    }

    /* The reading scrim: guarantees the copy zone whatever the photograph is,
       and adds a whisper of grain so the surface is not plastic. */
    .ksh-band::before {
      content: '';
      position: absolute;
      inset: 0;
      z-index: -1;
      background:
        repeating-linear-gradient(116deg, rgba(255, 255, 255, .02) 0 1px, transparent 1px 10px),
        linear-gradient(94deg, rgba(5, 10, 20, .9) 0%, rgba(5, 10, 20, .62) 44%, rgba(5, 10, 20, .2) 74%, transparent 96%),
        radial-gradient(70% 100% at 96% 4%, color-mix(in srgb, var(--ksh-red) 26%, transparent) 0%, transparent 62%);
      pointer-events: none;
    }

    .ksh-band-edge {
      position: absolute;
      left: 0;
      right: 0;
      bottom: 0;
      height: 3px;
      background: linear-gradient(90deg, transparent, var(--ksh-red) 22%, var(--ksh-gold) 50%, var(--ksh-red) 78%, transparent);
    }

    .ksh-wrap {
      width: 100%;
      max-width: 1260px;
      margin: 0 auto;
      padding-inline: clamp(1rem, 3.5vw, 2.5rem);
    }

    /* ══ HERO ═══════════════════════════════════════════════════════════════════ */
    .ksh-hero {
      display: grid;
      grid-template-columns: minmax(0, 1fr) auto;
      align-items: center;
      gap: clamp(1.5rem, 4.5vw, 3.5rem);
      padding-block: clamp(1.9rem, 4vw, 3.1rem) clamp(1.1rem, 2.2vw, 1.6rem);
    }

    /* No identity row means the hero itself has to clear the stat strip. */
    .ksh-hero:last-child {
      padding-bottom: clamp(3.4rem, 5.5vw, 4.4rem);
    }

    .ksh-eyebrow {
      display: inline-flex;
      align-items: center;
      gap: .6rem;
      font-size: .69rem;
      font-weight: 850;
      letter-spacing: .21em;
      text-transform: uppercase;
      color: var(--ksh-gold);
    }

    .ksh-eyebrow::before {
      content: '';
      width: 26px;
      height: 2px;
      background: linear-gradient(90deg, var(--ksh-red), var(--ksh-gold));
    }

    .ksh-title {
      margin: .7rem 0 0;
      font-family: var(--ks-display);
      font-size: clamp(2.4rem, 4.9vw, 3.9rem);
      font-weight: 700;
      line-height: 1.02;
      letter-spacing: -.015em;
      text-wrap: balance;
    }

    .ksh-lede {
      margin: .8rem 0 0;
      max-width: 44ch;
      font-size: clamp(1rem, 1.25vw, 1.1rem);
      line-height: 1.6;
      color: var(--ksh-on-dark-2);
    }

    .ksh-crest {
      justify-self: end;
    }

    .ksh-crest img {
      display: block;
      width: auto;
      max-width: clamp(230px, 30vw, 400px);
      max-height: clamp(195px, 25vw, 335px);
      object-fit: contain;
      filter: drop-shadow(0 22px 48px rgba(0, 0, 0, .6));
    }

    .ksh-crest img[data-media-state="empty"],
    .ksh-crest img[data-media-state="error"] {
      display: none;
    }

    .ksh-hero:not(:has(.ksh-crest img[data-media-state="present"])) {
      grid-template-columns: minmax(0, 1fr);
    }

    /* ── Identity, inside the band ─────────────────────────────────────────
       Ported from V3: the signed-in person belongs in the upper experience,
       not in a card halfway down. Here it sits on the band itself, which is
       what keeps it above the fold on a phone. */
    .ksh-id {
      display: flex;
      align-items: center;
      gap: clamp(.8rem, 2vw, 1.15rem);
      flex-wrap: wrap;
      padding-top: clamp(.9rem, 2vw, 1.15rem);
      /* Clears the stat strip, which is pulled up over the band's foot. */
      padding-bottom: clamp(3.4rem, 5.5vw, 4.4rem);
      border-top: 1px solid rgba(255, 255, 255, .14);
    }

    .ksh-id-mark {
      position: relative;
      width: 54px;
      height: 54px;
      flex-shrink: 0;
      border-radius: 50%;
      display: grid;
      place-items: center;
      background: linear-gradient(160deg, var(--ksh-raised), var(--ksh-deep));
      color: var(--ksh-gold);
      font-family: var(--ks-display);
      font-size: 1.08rem;
      font-weight: 700;
      /* The belt ring. Neutral when no belt is recorded -- a finished mark
         rather than a wrong one. */
      box-shadow: 0 0 0 2px rgba(5, 10, 20, .8), 0 0 0 4.5px var(--ksh-belt, rgba(255, 255, 255, .28));
    }

    .ksh-id-name {
      display: block;
      font-size: 1.06rem;
      font-weight: 850;
      line-height: 1.25;
      color: var(--ksh-on-dark);
    }

    .ksh-id-meta {
      display: flex;
      align-items: center;
      gap: .5rem;
      flex-wrap: wrap;
      margin-top: .28rem;
    }

    .ksh-role {
      padding: .2rem .58rem;
      border-radius: 999px;
      background: color-mix(in srgb, var(--ksh-gold) 20%, transparent);
      border: 1px solid color-mix(in srgb, var(--ksh-gold) 55%, transparent);
      color: var(--ksh-gold);
      font-size: .62rem;
      font-weight: 850;
      letter-spacing: .1em;
      text-transform: uppercase;
    }

    .ksh-belt {
      display: inline-flex;
      align-items: center;
      gap: .38rem;
      font-size: .78rem;
      font-weight: 700;
      color: var(--ksh-on-dark-2);
    }

    /* The swatch sits on a light plate. Without it a black or brown belt is a
       dark mark on a dark band -- invisible, which is exactly the failure the
       belt name is there to cover, and no reason to let the colour fail too. */
    .ksh-belt::before {
      content: '';
      width: 18px;
      height: 7px;
      border-radius: 2px;
      background: var(--ksh-belt-swatch, rgba(255, 255, 255, .35));
      box-shadow: 0 0 0 1.5px rgba(255, 255, 255, .9);
    }

    .ksh-id-actions {
      display: flex;
      gap: .55rem;
      margin-left: auto;
      flex-wrap: wrap;
    }

    /* ══ ACTIONS ════════════════════════════════════════════════════════════════ */
    .ksh-btn {
      display: inline-flex;
      align-items: center;
      justify-content: center;
      gap: .45rem;
      padding: .8rem 1.4rem;
      border-radius: var(--ks-r-control);
      border: 1px solid transparent;
      font-family: 'Raleway', sans-serif;
      font-size: .77rem;
      font-weight: 800;
      letter-spacing: .08em;
      text-transform: uppercase;
      text-decoration: none;
      white-space: nowrap;
      cursor: pointer;
      transition: background 150ms ease, border-color 150ms ease, color 150ms ease,
        transform 130ms cubic-bezier(.2, .7, .3, 1), box-shadow 160ms ease, filter 150ms ease;
    }

    .ksh-btn:hover {
      transform: translateY(-1px);
    }

    .ksh-btn:active {
      transform: translateY(1px);
      box-shadow: inset 0 2px 5px rgba(0, 0, 0, .28);
      filter: brightness(.96);
    }

    .ksh-btn-primary {
      background: linear-gradient(180deg, color-mix(in srgb, var(--ksh-red) 88%, white) 0%, var(--ksh-red) 60%, color-mix(in srgb, var(--ksh-red) 84%, black) 100%);
      border-color: color-mix(in srgb, var(--ksh-red) 70%, black);
      color: #fff;
      box-shadow: 0 1px 0 rgba(255, 255, 255, .22) inset, 0 10px 20px -12px rgba(0, 0, 0, .85);
    }

    .ksh-btn-primary:hover {
      filter: brightness(1.08);
    }

    .ksh-btn-gold {
      background: var(--ksh-gold-metal);
      border-color: color-mix(in srgb, var(--ksh-gold) 66%, black);
      color: #1B1403;
      box-shadow: 0 1px 0 rgba(255, 255, 255, .45) inset, 0 10px 20px -12px rgba(0, 0, 0, .8);
    }

    .ksh-btn-onband {
      background: rgba(255, 255, 255, .08);
      border-color: rgba(255, 255, 255, .3);
      color: var(--ksh-on-dark);
    }

    .ksh-btn-onband:hover {
      border-color: var(--ksh-gold);
      background: color-mix(in srgb, var(--ksh-gold) 16%, transparent);
    }

    .ksh-btn-quiet {
      background: var(--ksh-surface);
      border-color: var(--ksh-line);
      color: var(--ksh-ink);
    }

    .ksh-btn-quiet:hover {
      border-color: var(--ksh-gold);
      color: var(--ksh-gold-ink);
    }

    /* Every focusable thing inside this view, not only the ones this stylesheet authored.
       Shared controls that appear here -- the add-to-calendar trigger is one --
       get their focus ring from per-view rules elsewhere in the system, so
       without this they arrive here with none at all. */
    #view-tournament-hub :is(a, button, summary, select, input, [tabindex]):focus-visible {
      outline: 2px solid var(--ksh-gold);
      outline-offset: 2px;
      border-radius: var(--ks-r-control-sm);
    }

    .ksh-seg button:focus-visible {
      outline-offset: -2px;
    }

    /* ══ STAT STRIP ═════════════════════════════════════════════════════════════
       V1's signature: straddles the band's foot so the numbers belong to the
       dojo above rather than to the page below.
       ═══════════════════════════════════════════════════════════════════════════ */
    .ksh-stats-wrap {
      position: relative;
      z-index: 2;
      padding-inline: clamp(1rem, 3.5vw, 2.5rem);
      margin-top: clamp(-2.6rem, -3.6vw, -1.6rem);
    }

    .ksh-stats {
      max-width: 1260px;
      margin: 0 auto;
      display: grid;
      grid-auto-flow: column;
      grid-auto-columns: 1fr;
      background: var(--ksh-surface);
      border: 1px solid var(--ksh-line);
      border-radius: var(--ksh-r);
      box-shadow: inset 0 1px 0 rgba(255, 255, 255, .95), var(--ksh-shadow-lift);
      overflow: hidden;
      position: relative;
    }

    .ksh-stats::before {
      content: '';
      position: absolute;
      inset: 0 0 auto 0;
      height: 2px;
      background: var(--ksh-gold-metal);
      opacity: .85;
    }

    .ksh-stat {
      display: flex;
      align-items: center;
      justify-content: center;
      gap: .7rem;
      padding: clamp(.9rem, 1.8vw, 1.25rem) .75rem;
      text-align: left;
    }

    .ksh-stat + .ksh-stat {
      border-left: 1px solid var(--ksh-line-soft);
    }

    .ksh-stat-n {
      font-family: var(--ks-display);
      font-size: clamp(1.7rem, 3vw, 2.35rem);
      font-weight: 700;
      line-height: 1;
      color: var(--ksh-ink);
      font-variant-numeric: tabular-nums;
    }

    .ksh-stat-l {
      font-size: .63rem;
      font-weight: 850;
      letter-spacing: .12em;
      text-transform: uppercase;
      line-height: 1.35;
      color: var(--ksh-ink-3);
    }

    /* ══ ANNOUNCEMENT ═══════════════════════════════════════════════════════════
       Ported from V3. Full width, tenant colour, gold edge -- and absent from
       the document entirely when there is nothing to say, so no spacing is
       left behind.
       ═══════════════════════════════════════════════════════════════════════════ */
    .ksh-ann {
      margin-top: clamp(1.25rem, 2.5vw, 1.9rem);
      background: linear-gradient(100deg, var(--ksh-deep) 0%, var(--ksh-mid) 62%, var(--ksh-raised) 100%);
      border-block: 1px solid rgba(255, 255, 255, .09);
      color: var(--ksh-on-dark);
      position: relative;
    }

    .ksh-ann::before {
      content: '';
      position: absolute;
      inset: 0 auto 0 0;
      width: 4px;
      background: linear-gradient(180deg, var(--ksh-red), var(--ksh-gold));
    }

    .ksh-ann-in {
      display: flex;
      align-items: center;
      gap: clamp(.8rem, 2vw, 1.4rem);
      flex-wrap: wrap;
      padding-block: clamp(.9rem, 1.9vw, 1.15rem);
    }

    .ksh-ann-mark {
      display: grid;
      place-items: center;
      width: 34px;
      height: 34px;
      flex-shrink: 0;
      border-radius: 9px;
      background: color-mix(in srgb, var(--ksh-gold) 20%, transparent);
      color: var(--ksh-gold);
    }

    .ksh-ann-copy {
      flex: 1 1 20rem;
      min-width: 0;
    }

    .ksh-ann-k {
      display: block;
      font-size: .59rem;
      font-weight: 850;
      letter-spacing: .16em;
      text-transform: uppercase;
      color: var(--ksh-gold);
    }

    .ksh-ann-t {
      display: block;
      margin-top: .2rem;
      font-size: clamp(.95rem, 1.25vw, 1.06rem);
      font-weight: 650;
      line-height: 1.5;
    }

    /* ══ LAYOUT ═════════════════════════════════════════════════════════════════ */
    .ksh-body {
      padding-block: clamp(1.6rem, 3vw, 2.4rem) clamp(2.5rem, 5vw, 4rem);
    }

    .ksh-grid {
      display: grid;
      grid-template-columns: minmax(0, 1fr);
      gap: clamp(1rem, 2vw, 1.5rem);
    }

    @media (min-width: 1000px) {
      .ksh-grid {
        grid-template-columns: minmax(0, 1.6fr) minmax(0, 1fr);
        align-items: start;
      }
    }

    .ksh-col {
      display: grid;
      gap: clamp(1rem, 2vw, 1.5rem);
      align-content: start;
      min-width: 0;
    }

    .ksh-card {
      background: var(--ksh-surface);
      border: 1px solid var(--ksh-line);
      border-radius: var(--ksh-r);
      /* A one-pixel highlight along the top edge and a soft shade under the
         foot. It is the difference between a rectangle and a surface. */
      box-shadow: inset 0 1px 0 rgba(255, 255, 255, .95), var(--ksh-shadow);
      overflow: hidden;
    }

    .ksh-head {
      display: flex;
      align-items: center;
      gap: .6rem;
      padding: .85rem 1rem;
      border-bottom: 1px solid var(--ksh-line-soft);
    }

    .ksh-head .ks-icon {
      width: 1.05rem;
      height: 1.05rem;
      color: var(--ksh-gold-ink);
    }

    .ksh-head-t {
      font-size: .78rem;
      font-weight: 850;
      letter-spacing: .11em;
      text-transform: uppercase;
      color: var(--ksh-ink-2);
    }

    .ksh-head-link {
      margin-left: auto;
      background: none;
      border: 0;
      padding: .2rem;
      font: inherit;
      font-size: .69rem;
      font-weight: 850;
      letter-spacing: .07em;
      text-transform: uppercase;
      color: var(--ksh-gold-ink);
      cursor: pointer;
    }

    .ksh-head-link:hover {
      text-decoration: underline;
    }

    /* ══ ROWS ═══════════════════════════════════════════════════════════════════ */
    .ksh-rail {
      display: grid;
    }

    .ksh-row {
      display: flex;
      align-items: center;
      gap: .8rem;
      width: 100%;
      padding: .78rem 1rem;
      background: none;
      border: 0;
      border-top: 1px solid var(--ksh-line-soft);
      text-align: left;
      font: inherit;
      color: inherit;
      cursor: pointer;
      transition: background 140ms ease;
    }

    .ksh-rail > .ksh-row:first-child {
      border-top: 0;
    }

    .ksh-row {
      position: relative;
      transition: background 140ms ease, box-shadow 160ms ease;
    }

    .ksh-row:hover {
      background: var(--ksh-sunken);
      box-shadow: inset 3px 0 0 var(--ksh-gold);
    }

    button.ksh-row:active {
      background: var(--ksh-line-soft);
      box-shadow: inset 3px 0 0 var(--ksh-gold), inset 0 1px 3px rgba(16, 27, 48, .12);
    }

    .ksh-row:hover .ksh-date {
      box-shadow: 0 0 0 1.5px color-mix(in srgb, var(--ksh-gold) 65%, transparent);
    }

    .ksh-date {
      transition: box-shadow 160ms ease;
    }

    .ksh-date {
      flex-shrink: 0;
      width: 46px;
      padding: .3rem 0 .36rem;
      border-radius: 9px;
      text-align: center;
      background: linear-gradient(180deg, var(--ksh-mid), var(--ksh-deep));
      color: #fff;
    }

    .ksh-date-m {
      display: block;
      font-size: .54rem;
      font-weight: 900;
      letter-spacing: .12em;
      color: var(--ksh-gold);
    }

    .ksh-date-d {
      display: block;
      font-family: var(--ks-display);
      font-size: 1.1rem;
      line-height: 1.05;
      font-variant-numeric: tabular-nums;
    }

    .ksh-row-copy {
      min-width: 0;
    }

    .ksh-row-t {
      display: block;
      font-size: .9rem;
      font-weight: 800;
      line-height: 1.3;
      color: var(--ksh-ink);
      overflow: hidden;
      text-overflow: ellipsis;
      white-space: nowrap;
    }

    .ksh-row-m {
      display: block;
      margin-top: .12rem;
      font-size: .77rem;
      color: var(--ksh-ink-3);
    }

    .ksh-pill {
      margin-left: auto;
      flex-shrink: 0;
      padding: .25rem .6rem;
      border-radius: 999px;
      background: var(--ksh-sunken);
      border: 1px solid var(--ksh-line);
      color: var(--ksh-ink-2);
      font-size: .63rem;
      font-weight: 850;
      white-space: nowrap;
    }

    .ksh-pill-go {
      background: #ECFDF5;
      border-color: #A7F3D0;
      color: #0F6B42;
    }

    .ksh-pill-open {
      background: color-mix(in srgb, var(--ksh-red) 9%, transparent);
      border-color: color-mix(in srgb, var(--ksh-red) 38%, transparent);
      color: var(--ksh-red);
    }

    /* ══ CENTREPIECE ════════════════════════════════════════════════════════════ */
    .ksh-feature {
      box-shadow: var(--ksh-shadow-lift);
    }

    .ksh-cover {
      position: relative;
      min-height: clamp(126px, 17vw, 196px);
      padding: 1.1rem 1.15rem;
      background:
        radial-gradient(96% 150% at 4% 112%, color-mix(in srgb, var(--ksh-gold) 26%, transparent) 0%, transparent 56%),
        radial-gradient(80% 120% at 98% 0%, color-mix(in srgb, var(--ksh-red) 30%, transparent) 0%, transparent 58%),
        linear-gradient(150deg, var(--ksh-deep) 0%, var(--ksh-mid) 56%, var(--ksh-raised) 100%);
      color: var(--ksh-on-dark);
      overflow: hidden;
      display: flex;
      align-items: flex-end;
      box-shadow: inset 0 1px 0 rgba(255, 255, 255, .16);
    }

    .ksh-cover-img {
      position: absolute;
      inset: 0;
      width: 100%;
      height: 100%;
      object-fit: cover;
      object-position: center 32%;
      mask-image: linear-gradient(102deg, transparent 0%, #000 48%);
    }

    .ksh-cover-in {
      position: relative;
      width: 100%;
    }

    .ksh-cover-k {
      display: block;
      font-size: .6rem;
      font-weight: 900;
      letter-spacing: .19em;
      text-transform: uppercase;
      color: var(--ksh-gold);
    }

    .ksh-cover-t {
      margin: .4rem 0 0;
      font-family: var(--ks-display);
      font-size: clamp(1.45rem, 2.5vw, 2.1rem);
      font-weight: 700;
      line-height: 1.08;
      max-width: 20ch;
    }

    .ksh-cover-count {
      position: absolute;
      top: 0;
      right: 0;
      padding: .36rem .72rem;
      border-radius: 999px;
      background: var(--ksh-red);
      color: #fff;
      font-size: .68rem;
      font-weight: 850;
      white-space: nowrap;
      box-shadow: 0 8px 18px -10px rgba(0, 0, 0, .9);
    }

    .ksh-cover-open {
      position: absolute;
      right: 0;
      bottom: 0;
      padding: .24rem .62rem;
      border-radius: 999px;
      background: rgba(4, 8, 16, .78);
      color: #fff;
      font-size: .56rem;
      font-weight: 800;
      letter-spacing: .1em;
      text-transform: uppercase;
    }

    .ksh-facts {
      display: grid;
      grid-template-columns: repeat(2, minmax(0, 1fr));
      border-bottom: 1px solid var(--ksh-line);
    }

    @media (min-width: 700px) {
      .ksh-facts {
        grid-template-columns: repeat(4, minmax(0, 1fr));
      }
    }

    .ksh-fact {
      padding: .82rem 1rem;
      border-left: 1px solid var(--ksh-line-soft);
      min-width: 0;
    }

    .ksh-fact:first-child {
      border-left: 0;
    }

    .ksh-fact-k {
      display: block;
      font-size: .57rem;
      font-weight: 850;
      letter-spacing: .13em;
      text-transform: uppercase;
      color: var(--ksh-ink-3);
    }

    .ksh-fact-v {
      display: block;
      margin-top: .2rem;
      font-size: .87rem;
      font-weight: 750;
      line-height: 1.3;
      color: var(--ksh-ink);
    }

    .ksh-feature-body {
      padding: 1rem 1.1rem 1.1rem;
    }

    .ksh-coach {
      display: flex;
      gap: .7rem;
      padding: .85rem .95rem;
      border-radius: var(--ksh-r-sm);
      background: color-mix(in srgb, var(--ksh-gold) 9%, var(--ksh-surface));
      border: 1px solid color-mix(in srgb, var(--ksh-gold) 32%, transparent);
      border-left: 3px solid var(--ksh-gold);
    }

    .ksh-coach .ks-icon {
      color: var(--ksh-gold-ink);
      flex-shrink: 0;
    }

    .ksh-coach-k {
      display: block;
      font-size: .58rem;
      font-weight: 900;
      letter-spacing: .13em;
      text-transform: uppercase;
      color: var(--ksh-gold-ink);
    }

    .ksh-coach-t {
      display: block;
      margin-top: .2rem;
      font-size: .9rem;
      line-height: 1.55;
      font-weight: 600;
      color: var(--ksh-ink);
    }

    .ksh-rsvp {
      margin-top: .85rem;
      padding: .85rem .95rem;
      border-radius: var(--ksh-r-sm);
      background: var(--ksh-sunken);
      border: 1px solid var(--ksh-line);
    }

    /* A question, set as one: the stamped .6rem label it was is what the
       authenticated surface is moving away from. */
    .ksh-rsvp-k {
      font-size: .88rem;
      font-weight: 700;
      color: var(--ksh-ink);
    }

    .ksh-seg {
      display: grid;
      grid-template-columns: repeat(3, minmax(0, 1fr));
      gap: 4px;
      margin-top: .5rem;
      padding: 4px;
      background: var(--ksh-surface);
      border: 1px solid var(--ksh-line);
      border-radius: 9px;
    }

    .ksh-seg button {
      padding: .58rem .3rem;
      border: 0;
      border-radius: var(--ks-r-control-sm);
      background: none;
      font-family: inherit;
      font-size: .74rem;
      font-weight: 800;
      color: var(--ksh-ink-2);
      cursor: pointer;
      transition: background 140ms ease, color 140ms ease;
    }

    .ksh-seg button:hover {
      background: var(--ksh-sunken);
    }

    .ksh-seg button {
      position: relative;
      transition: background 140ms ease, color 140ms ease, box-shadow 160ms ease;
    }

    .ksh-seg button:active {
      box-shadow: inset 0 2px 4px rgba(16, 27, 48, .16);
    }

    .ksh-seg button.is-on {
      background: linear-gradient(180deg, var(--ksh-mid), var(--ksh-deep));
      color: #fff;
      box-shadow: 0 4px 10px -5px rgba(5, 10, 20, .8);
    }

    /* The one gold line that says "this is your answer". */
    .ksh-seg button.is-on::after {
      content: '';
      position: absolute;
      left: 22%;
      right: 22%;
      bottom: 5px;
      height: 2px;
      border-radius: var(--ks-r-control-sm);
      background: var(--ksh-gold-metal);
    }

    .ksh-actions {
      display: flex;
      flex-wrap: wrap;
      gap: .55rem;
      margin-top: .9rem;
    }

    /* ══ DISCLOSURE ═════════════════════════════════════════════════════════════ */
    .ksh-disc {
      margin-top: .85rem;
      border-top: 1px solid var(--ksh-line);
    }

    .ksh-disc-sum {
      list-style: none;
      cursor: pointer;
      position: relative;
      padding: .8rem 2rem .2rem 0;
      font-size: .65rem;
      font-weight: 850;
      letter-spacing: .13em;
      text-transform: uppercase;
      color: var(--ksh-ink-2);
    }

    .ksh-disc-sum::-webkit-details-marker {
      display: none;
    }

    .ksh-disc-sum:hover {
      color: var(--ksh-gold-ink);
    }

    .ksh-disc-sum:hover::after {
      border-color: var(--ksh-gold-ink);
    }

    .ksh-disc-sum::after {
      content: '';
      position: absolute;
      right: .35rem;
      top: 1.12rem;
      width: 7px;
      height: 7px;
      border-right: 2px solid var(--ksh-ink-3);
      border-bottom: 2px solid var(--ksh-ink-3);
      rotate: 45deg;
      transition: rotate 170ms ease;
    }

    .ksh-disc[open] > .ksh-disc-sum::after {
      rotate: -135deg;
    }

    .ksh-note {
      margin-top: .7rem;
      padding-left: .85rem;
      border-left: 2px solid var(--ksh-line);
    }

    .ksh-note-k {
      display: block;
      font-size: .57rem;
      font-weight: 850;
      letter-spacing: .12em;
      text-transform: uppercase;
      color: var(--ksh-ink-3);
    }

    .ksh-note-t {
      display: block;
      margin-top: .18rem;
      font-size: .85rem;
      line-height: 1.55;
      color: var(--ksh-ink-2);
    }

    /* ══ TRAVEL ═════════════════════════════════════════════════════════════════ */
    .ksh-travel-img {
      display: block;
      width: 100%;
      height: 128px;
      object-fit: cover;
      background: linear-gradient(150deg, var(--ksh-mid), var(--ksh-deep));
    }

    .ksh-pad {
      padding: .95rem 1rem 1rem;
    }

    .ksh-travel-n {
      display: block;
      font-size: 1rem;
      font-weight: 850;
      color: var(--ksh-ink);
    }

    .ksh-travel-w {
      display: block;
      margin-top: .14rem;
      font-size: .79rem;
      font-weight: 700;
      color: var(--ksh-ink-3);
    }

    /* ══ EMPTY ══════════════════════════════════════════════════════════════════ */
    .ksh-empty {
      padding: 1.6rem 1.1rem;
      text-align: center;
    }

    .ksh-empty strong {
      display: block;
      font-size: .97rem;
      font-weight: 800;
      color: var(--ksh-ink);
    }

    .ksh-empty span {
      display: block;
      margin-top: .32rem;
      font-size: .84rem;
      line-height: 1.55;
      color: var(--ksh-ink-3);
    }

    /* ══ MOTION ═════════════════════════════════════════════════════════════════
       Two signatures: the band settles once on arrival (crest, copy, identity,
       stats), and each section lifts as it is reached. Opacity and transform
       only, one-shot, and neither runs under reduced motion.
       ═══════════════════════════════════════════════════════════════════════════ */
    @media (prefers-reduced-motion: no-preference) {

      /* The route arrives. Opacity only -- nothing moves, so nothing shifts. */
      #view-tournament-hub.active {
        animation: kshEnter .24s ease backwards;
      }

      #view-tournament-hub.active .ksh-hero-copy,
      #view-tournament-hub.active .ksh-id,
      #view-tournament-hub.active .ksh-stats {
        animation: kshRise .58s cubic-bezier(.2, .7, .3, 1) backwards;
      }

      /* The crest settles in rather than sliding: a small scale and a fade,
         once, on arrival. */
      #view-tournament-hub.active .ksh-crest {
        animation: kshCrest .72s cubic-bezier(.16, .84, .32, 1) backwards;
      }

      #view-tournament-hub.active .ksh-hero-copy { animation-delay: .07s; }
      #view-tournament-hub.active .ksh-id { animation-delay: .14s; }
      #view-tournament-hub.active .ksh-stats { animation-delay: .2s; }

      #view-tournament-hub[data-ksh-reveal="on"] .ksh-sec {
        opacity: 0;
        translate: 0 15px;
        transition: opacity .5s ease, translate .5s cubic-bezier(.2, .7, .3, 1);
      }

      #view-tournament-hub[data-ksh-reveal="on"] .ksh-sec.is-in {
        opacity: 1;
        translate: 0 0;
      }
    }

    @keyframes kshRise {
      from { opacity: 0; translate: 0 15px; }
      to { opacity: 1; translate: 0 0; }
    }

    @keyframes kshCrest {
      from { opacity: 0; scale: .955; }
      to { opacity: 1; scale: 1; }
    }

    @keyframes kshEnter {
      from { opacity: 0; }
      to { opacity: 1; }
    }

    /* Progressive disclosure, where the browser can animate to a natural
       height. Everywhere else the panel simply opens, which is the correct
       fallback and costs nothing. */
    @media (prefers-reduced-motion: no-preference) {
      @supports selector(::details-content) {
        .ksh-disc::details-content {
          block-size: 0;
          overflow: clip;
          transition:
            block-size .34s cubic-bezier(.2, .7, .3, 1),
            content-visibility .34s allow-discrete;
        }

        .ksh-disc[open]::details-content {
          block-size: auto;
        }
      }
    }

    /* ══ MOBILE ═════════════════════════════════════════════════════════════════
       The fixed order, enforced on the panels themselves:
         hero identity → announcement → This Week → next tournament →
         season context → your events → upcoming tournaments →
         the coach's roll-up → travel
       (the last four mirror the desktop column's own sequence; a section
       missing from this list defaults to order 0 and jumps the queue, which
       is exactly what Your Events did until 2026-08-23)
       ═══════════════════════════════════════════════════════════════════════════ */
    @media (max-width: 999px) {
      .ksh-grid {
        display: flex;
        flex-direction: column;
      }

      .ksh-col {
        display: contents;
      }

      .ksh-s-week { order: 1; }
      .ksh-s-next { order: 2; }
      .ksh-s-context { order: 3; }
      .ksh-s-answers { order: 4; }
      .ksh-s-upcoming { order: 5; }
      .ksh-s-rollup { order: 6; }
      .ksh-s-travel { order: 7; }
    }

    @media (max-width: 639px) {

      /* Identity centres; operational content stays left. */
      /* Roughly a tenth off the hero's height, taken from the crest, the
         gaps and the leading rather than from the type -- so the announcement
         and the first rows of This Week arrive sooner without the identity
         or the dojo's name getting smaller in any way that matters. */
      .ksh-hero {
        grid-template-columns: minmax(0, 1fr);
        justify-items: center;
        text-align: center;
        gap: .8rem;
        padding-block: 1.25rem .45rem;
      }

      .ksh-title {
        font-size: 2.15rem;
      }

      .ksh-hero .ksh-lede {
        margin-top: .55rem;
        line-height: 1.45;
      }

      .ksh-crest {
        justify-self: center;
        order: -1;
      }

      .ksh-crest img {
        max-width: min(54vw, 205px);
        max-height: 150px;
      }

      .ksh-lede {
        margin-inline: auto;
        font-size: .96rem;
      }

      .ksh-id {
        flex-direction: column;
        text-align: center;
        gap: .5rem;
        padding-top: .75rem;
        padding-bottom: 2.6rem;
      }

      .ksh-id-mark {
        width: 46px;
        height: 46px;
        font-size: .95rem;
      }

      .ksh-id-meta {
        justify-content: center;
      }

      .ksh-id-actions {
        margin-left: 0;
        width: 100%;
        justify-content: center;
      }

      .ksh-id-actions .ksh-btn {
        flex: 1 1 auto;
      }

      .ksh-stat {
        flex-direction: column;
        gap: .25rem;
        text-align: center;
        padding-inline: .4rem;
      }

      .ksh-stat-l {
        font-size: .55rem;
        letter-spacing: .08em;
      }

      .ksh-ann-in,
      .ksh-row,
      .ksh-fact,
      .ksh-feature-body,
      .ksh-pad {
        text-align: left;
      }
    }

    /* ══ ACCESS STATES ══════════════════════════════════════════════════════════
       Signed out, membership resolving, and the read having failed. One card,
       centred under the band, never a blank column. Sized so the page does not
       collapse to a strip on a wide screen.
       ═══════════════════════════════════════════════════════════════════════════ */
    .ksh-access {
      max-width: 34rem;
      margin-inline: auto;
      padding: clamp(1.6rem, 3.5vw, 2.4rem);
      text-align: center;
    }

    .ksh-access-k {
      display: block;
      font-size: .62rem;
      font-weight: 850;
      letter-spacing: .16em;
      text-transform: uppercase;
      color: var(--ksh-gold-ink);
    }

    .ksh-access-t {
      display: block;
      margin-top: .5rem;
      font-family: var(--ks-display);
      font-size: clamp(1.25rem, 2.2vw, 1.6rem);
      font-weight: 700;
      line-height: 1.15;
      color: var(--ksh-ink);
    }

    .ksh-access-m {
      display: block;
      margin-top: .55rem;
      font-size: .92rem;
      line-height: 1.6;
      color: var(--ksh-ink-2);
    }

    .ksh-access-a {
      justify-content: center;
    }

    /* ══ THE COACH'S ROLL-UP ════════════════════════════════════════════════════
       The shared summary component renders inside this. It is closed by default
       so a coach on a phone is not scrolling past every answer to reach the
       tournament they came for.
       ═══════════════════════════════════════════════════════════════════════════ */
    .ksh-disc-flush {
      margin-top: 0;
      border-top: 0;
      padding-inline: 1rem;
    }

    .ksh-rollup {
      padding-bottom: .9rem;
      /* The summary brings its own list styling from the shared system; all
         this owes it is somewhere to go and a way out on a narrow screen. */
      overflow-x: auto;
    }


/* ══════════════════════════════════════════════════════════════════════
   dojo-store-promo-banner.v1.css
   ══════════════════════════════════════════════════════════════════════ */
    /* ── STORE PROMO BANNER ─────────────────────────────────────────────────── */


/* ══════════════════════════════════════════════════════════════════════
   dojo-content-lifecycle-status.v1.css
   ══════════════════════════════════════════════════════════════════════ */
    /* ──────────────────────────────────────────────────────────────────────────
       Content lifecycle status markers.

       These only ever render for coaches: every public surface filters archived
       and past records out entirely, so a badge here is a signal to whoever is
       managing history, not decoration for members.

       "Archived" and "Past" are styled apart on purpose. Archived is a status a
       coach chose and can reverse; past is a date, and only a new date changes
       it. Reading the same colour for both would hide that difference.
       ────────────────────────────────────────────────────────────────────────── */

    .ks-content-status-badge {
      display: inline-flex;
      align-items: center;
      gap: .35rem;
      align-self: flex-start;
      /* In the event card's grid the badge is an unplaced item; without this it
         stretches into a full-width bar. Ignored by flex parents. */
      justify-self: start;
      margin-bottom: .6rem;
      padding: .28rem .7rem;
      border-radius: 999px;
      font-size: .64rem;
      font-weight: 850;
      letter-spacing: 1.1px;
      text-transform: uppercase;
      line-height: 1.4;
      white-space: nowrap;
    }

    .ks-content-status-badge.is-archived {
      background: #E2E8F0;
      color: #334155;
      border: 1px solid #CBD5E1;
    }

    .ks-content-status-badge.is-past {
      background: #FEF3C7;
      color: #78350F;
      border: 1px solid #FCD34D;
    }

    /* Shown in place of a Restore button on an archived record whose date has
       also passed. Restoring it would leave it hidden anyway, so the row states
       the reason instead of offering a control that could only fail. */
    .ks-tourney-admin-note {
      display: inline-flex;
      align-items: center;
      min-height: 38px;
      padding: .58rem .85rem;
      font-size: .66rem;
      font-weight: 800;
      letter-spacing: .6px;
      text-transform: uppercase;
      color: #64748B;
    }


/* ══════════════════════════════════════════════════════════════════════
   ks-shipped-surfaces.v1.css
   ══════════════════════════════════════════════════════════════════════ */
/* ──────────────────────────────────────────────────────────────────────────
   Styles for the surfaces added 2026-08-03: the notification bell, the
   contact inbox, division bands, instructors, guardians and cross-dojo
   search.

   They shipped with none, which a reachability audit caught rather than a
   design review — every one rendered as unstyled DOM on a product whose
   recorded direction is a premium light-first language. This is that debt
   paid in one file, using the tokens the rest of the system already speaks
   (--ks-surface, --ks-line, --ks-ink, --gold) so these panels inherit any
   later change to them instead of pinning their own colours.

   THE [hidden] RULES ARE NOT DECORATION. `hidden` is only a user-agent
   `display: none`, and any authored `display` beats it — the bell is an <li>
   in a nav whose rule laid it out regardless, so an empty bell showed for
   every signed-in member. The audit found that; these rules fix it, and the
   codebase already uses the same guard on .ks-tenant-lifecycle-shell[hidden].
   ────────────────────────────────────────────────────────────────────────── */

/* ── the hidden contract, first, because it is correctness ───────────────── */
.ks-notif-host[hidden],
.ks-cross-search-section[hidden],
.ks-instructors-section[hidden] {
  display: none !important;
}

/* ── notification bell ───────────────────────────────────────────────────── */
.ks-notif-host {
  position: relative;
  display: flex;
  align-items: center;
}

/* The search action, built to the bell's geometry so the pair reads as one
   cluster rather than two controls that happen to be adjacent. Same target
   size, same hover, same pill — the only difference is what it opens. Hidden
   below the drawer breakpoint, where Search lives in the menu list instead. */
.ks-nav-search {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border: 0;
  background: transparent;
  color: var(--ks-band-ink-2, rgba(255, 255, 255, .82));
  cursor: pointer;
  font-size: 1.05rem;
  line-height: 1;
  padding: .35rem .5rem;
  border-radius: 999px;
  transition: background-color .18s var(--ease-out, ease-out), color .18s var(--ease-out, ease-out);
}

.ks-nav-search:hover,
.ks-nav-search:focus-visible {
  color: var(--ks-band-ink, #fff);
  background: color-mix(in srgb, var(--ks-accent) 16%, transparent);
}

.ks-nav-search:focus-visible {
  outline: 2px solid var(--ks-band-accent, var(--ks-focus-ring));
  outline-offset: 2px;
}

/* The drawer breakpoint. Below it the bar collapses to the dock and there is
   nowhere for this to sit, so the menu's Search entry answers instead — the
   two are never both offered. */
@media (max-width: 900px) {
  .ks-nav-search {
    display: none;
  }
}

.ks-notif-bell {
  position: relative;
  border: 0;
  background: transparent;
  cursor: pointer;
  font-size: 1.05rem;
  line-height: 1;
  padding: .35rem .5rem;
  border-radius: 999px;
  transition: background-color .18s var(--ease-out, ease-out);
}

.ks-notif-bell:hover,
.ks-notif-bell:focus-visible {
  background: color-mix(in srgb, var(--ks-accent) 16%, transparent);
}

/* The count sits on the bell rather than beside it: a badge that reflows the
   nav every time a notification arrives is worse than no badge. */
.ks-notif-badge {
  position: absolute;
  top: -.15rem;
  right: -.1rem;
  min-width: 1.05rem;
  padding: 0 .25rem;
  border-radius: 999px;
  background: var(--ks-accent);
  color: #1a1206;
  font-size: .6rem;
  font-weight: 800;
  line-height: 1.05rem;
  text-align: center;
}

.ks-notif-panel {
  position: absolute;
  top: calc(100% + .55rem);
  right: 0;
  /* In the token scale, not beside it: above overlays and the lifecycle
     shells (an open panel is the user's current intent), below --z-toast —
     action feedback stays supreme and fades on its own. The bell also
     dismisses any showing toast when the panel OPENS, because on a phone
     both anchor under the nav and a welcome toast sitting on top of a
     deliberately opened inbox reads as clutter. */
  z-index: calc(var(--z-modal, 400) + 10);
  width: min(22rem, calc(100vw - 2rem));
  max-height: 24rem;
  overflow-y: auto;
  background: var(--ks-surface, #fff);
  /* Set here, at the root, not per descendant: the panel hangs inside the
     NAV, whose ink is tuned for the dark bar — the heading inherited it and
     rendered washed-out on this white surface. Every child now starts from
     the surface's own ink, whatever the tenant theme does to the bar. */
  color: var(--ks-ink, #1c1c1c);
  text-align: left;
  border: 1px solid var(--ks-line, rgba(0, 0, 0, .1));
  border-radius: .8rem;
  box-shadow: 0 18px 44px rgba(0, 0, 0, .16);
}

.ks-notif-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: .75rem;
  padding: .7rem .85rem;
  border-bottom: 1px solid var(--ks-line, rgba(0, 0, 0, .08));
}

.ks-notif-allread {
  border: 0;
  background: transparent;
  color: var(--ks-accent, #8a6d3b);
  font-size: .7rem;
  font-weight: 700;
  cursor: pointer;
}

/* A real <button> since v2, so the keyboard gets each row for free — the
   resets below undo the UA button chrome without hiding that it is one. */
.ks-notif-item {
  display: block;
  width: 100%;
  text-align: left;
  font: inherit;
  color: inherit;
  background: none;
  border: 0;
  padding: .7rem .85rem;
  border-bottom: 1px solid var(--ks-line, rgba(0, 0, 0, .06));
  cursor: pointer;
}

.ks-notif-item:last-child { border-bottom: 0; }
.ks-notif-item:hover { background: var(--ks-surface-sunken, rgba(0, 0, 0, .03)); }
.ks-notif-item:focus-visible {
  outline: 2px solid var(--ks-focus-ring, #2563eb);
  outline-offset: -2px;
}

/* Unread is carried by weight and a rail, never by colour alone — the
   distinction has to survive a reader who cannot separate the two. */
.ks-notif-unread {
  border-left: 3px solid var(--ks-accent);
  padding-left: calc(.85rem - 3px);
}

.ks-notif-title { font-size: .78rem; font-weight: 700; color: var(--ks-ink, #1c1c1c); }
.ks-notif-unread .ks-notif-title { font-weight: 850; }

/* Cross-dojo context: the tenant slug on any row that is not about the dojo
   on screen, so one user-level inbox can span tenants without any item
   pretending to be local. Token-derived like every chip in the system. */
.ks-notif-dojo {
  display: inline-block;
  margin-left: .45rem;
  padding: .08rem .45rem;
  border-radius: 999px;
  border: 1px solid var(--ks-line, rgba(0, 0, 0, .1));
  background: var(--ks-surface-sunken, rgba(0, 0, 0, .04));
  color: var(--muted, #6b7280);
  font-size: .62rem;
  font-weight: 800;
  letter-spacing: .04em;
  text-transform: uppercase;
  vertical-align: middle;
}
.ks-notif-body { font-size: .72rem; color: var(--muted, #6b7280); margin-top: .15rem; }
.ks-notif-when { font-size: .64rem; color: var(--muted, #6b7280); margin-top: .3rem; }
.ks-notif-footnote { padding: .6rem .85rem; font-size: .66rem; }

/* ── contact inbox ───────────────────────────────────────────────────────── */
.ks-inbox-card {
  padding: .8rem .9rem;
  border: 1px solid var(--ks-line, rgba(0, 0, 0, .1));
  border-radius: .7rem;
  margin-bottom: .6rem;
  background: var(--ks-surface, #fff);
}

.ks-inbox-handled { opacity: .62; }

.ks-inbox-head {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: .5rem .8rem;
  margin-bottom: .4rem;
}

.ks-inbox-mail { color: var(--ks-accent, #8a6d3b); font-size: .74rem; }
.ks-inbox-when { margin-left: auto; font-size: .66rem; }

/* A stranger's message keeps its own line breaks and never escapes its card,
   however it was typed. */
.ks-inbox-message {
  white-space: pre-wrap;
  overflow-wrap: anywhere;
  font-size: .76rem;
  line-height: 1.5;
  color: var(--ks-ink, #1c1c1c);
  margin: 0 0 .55rem;
}

.ks-inbox-actions { display: flex; gap: .45rem; flex-wrap: wrap; }

/* ── division bands ──────────────────────────────────────────────────────── */
.ks-bands-row,
.ks-guard-row,
.ks-instr-controls {
  display: flex;
  align-items: center;
  gap: .5rem;
}

.ks-bands-row {
  justify-content: space-between;
  padding: .45rem .65rem;
  border: 1px solid var(--ks-line, rgba(0, 0, 0, .09));
  border-radius: .55rem;
  margin-bottom: .35rem;
}

.ks-bands-label { font-size: .78rem; font-weight: 650; }
.ks-bands-controls { display: flex; gap: .3rem; }
.ks-bands-add { display: flex; gap: .5rem; margin: .6rem 0; }
.ks-bands-add input { flex: 1; min-width: 0; }

/* ── instructors ─────────────────────────────────────────────────────────── */
.ks-instr-card {
  padding: .75rem;
  border: 1px solid var(--ks-line, rgba(0, 0, 0, .1));
  border-radius: .7rem;
  margin-bottom: .6rem;
}

.ks-instr-row { display: flex; gap: .5rem; margin-bottom: .45rem; flex-wrap: wrap; }
.ks-instr-input { flex: 1; min-width: 8rem; }
.ks-instr-actions { display: flex; gap: .5rem; margin-top: .5rem; }

.ks-instructors-section { margin: 2.5rem auto; max-width: 72rem; padding: 0 1rem; }
.ks-instructors-title { font-size: 1.1rem; font-weight: 800; margin-bottom: .9rem; }

.ks-instructors-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(15rem, 1fr));
  gap: .9rem;
}

.ks-instructor-card {
  padding: .9rem 1rem;
  border: 1px solid var(--ks-line, rgba(0, 0, 0, .1));
  border-radius: .8rem;
  background: var(--ks-surface, #fff);
}

.ks-instructor-name { font-weight: 800; font-size: .92rem; }
.ks-instructor-meta { font-size: .74rem; color: var(--ks-accent, #8a6d3b); margin-top: .12rem; }
.ks-instructor-years { font-size: .68rem; color: var(--muted, #6b7280); margin-top: .1rem; }
.ks-instructor-bio { font-size: .76rem; line-height: 1.5; margin: .5rem 0 0; }

/* ── guardians ───────────────────────────────────────────────────────────── */
.ks-guard-row { justify-content: space-between; padding: .4rem 0; }
.ks-guard-name { font-size: .78rem; }
.ks-guard-controls { display: flex; gap: .5rem; margin-top: .5rem; flex-wrap: wrap; }
.ks-guard-picker { flex: 1; min-width: 10rem; }
.ks-guard-note { font-size: .7rem; margin: .2rem 0 .6rem; }

/* ── cross-dojo search ───────────────────────────────────────────────────── */
.ks-cross-search-section {
  margin-top: 1rem;
  padding: .85rem;
  border: 1px solid var(--ks-line, rgba(0, 0, 0, .1));
  border-radius: .8rem;
  background: var(--ks-surface-sunken, rgba(0, 0, 0, .02));
}

/* Reads as a section label, in ink — not in the accent, which on this sunken
   panel washed out to look disabled. */
.ks-cross-search-title {
  font-size: .72rem;
  font-weight: 800;
  letter-spacing: .08em;
  text-transform: uppercase;
  color: var(--ks-ink-2, #475467);
  margin-bottom: .5rem;
}

.ks-xsearch-controls { display: flex; gap: .5rem; }

/* Same shape as the roster's .ks-search pill above it, so the two search
   fields on this screen read as one family. */
.ks-xsearch-input {
  flex: 1;
  min-width: 0;
  background: var(--ks-surface, #fff);
  border: 1px solid var(--ks-line, rgba(0, 0, 0, .1));
  border-radius: var(--radius-full, 999px);
  padding: .5rem .95rem;
  color: var(--ks-ink, #101828);
  font-size: .85rem;
  outline: none;
}

.ks-xsearch-input:focus-visible {
  border-color: var(--ks-accent);
  box-shadow: 0 0 0 3px var(--ks-accent-soft, rgba(0, 0, 0, .08));
}

.ks-xsearch-input::placeholder {
  color: var(--ks-ink-3, #667085);
}
.ks-xsearch-group { margin-top: .7rem; }

/* The dojo a result belongs to is never implied by grouping alone — a coach
   reading two rosters at once has to see which is which without counting. */
.ks-xsearch-dojo {
  font-size: .66rem;
  font-weight: 800;
  letter-spacing: .06em;
  text-transform: uppercase;
  color: var(--ks-accent, #8a6d3b);
  margin-bottom: .25rem;
}

.ks-xsearch-row {
  display: flex;
  justify-content: space-between;
  gap: .75rem;
  padding: .35rem 0;
  border-bottom: 1px solid var(--ks-line, rgba(0, 0, 0, .06));
  font-size: .76rem;
}

.ks-xsearch-name { font-weight: 650; }
.ks-xsearch-count,
.ks-xsearch-status,
.ks-bands-status,
.ks-instr-status,
.ks-guard-status { font-size: .68rem; margin-top: .5rem; }

/* ── the relabelled toggle's note ────────────────────────────────────────── */
.ta-switch-note {
  font-size: .66rem;
  color: var(--muted, #6b7280);
  margin: .15rem 0 .7rem 1.9rem;
  line-height: 1.45;
}

/* ── system health ───────────────────────────────────────────────────────── */
/* The System Health panel's styles moved to dojo-platform-owner-dashboard.v1.css
   on 2026-08-16 — its only mount is inside the platform console, and one
   console's styling was split across two bundles for no reason but history. */


/* ══════════════════════════════════════════════════════════════════════
   dojo-responsive-command-dock-mobile.v1.css
   ══════════════════════════════════════════════════════════════════════ */
    /* ── RESPONSIVE — COMMAND DOCK MOBILE ──────────────────────────────────── */
    /*
     * 900px, and the number is MEASURED rather than chosen.
     *
     * It was 1100px, which put a phone-style bottom dock on laptops and
     * landscape tablets — a 1100px window is a desktop browser, and it was
     * being given the narrow-layout navigation. Measured on the real homann
     * header (signed out, five nav items + Resources + Log In), stepping the
     * viewport down with this query disabled so the desktop nav's true fit
     * could be read:
     *
     *   width   brand↔nav gap   logo    dojo name
     *   1024    88px            120px   intact
     *   950     53px            120px   intact
     *   900     29px            120px   intact      <- last good width
     *   880     19px             98px   intact      <- logo already shrinking
     *   864     11px             98px   intact
     *   840      8px (floor)     98px   TRUNCATED
     *   820      8px (floor)     98px   TRUNCATED
     *
     * Two independent signals agree on the same place. Below 900 the brand mark
     * starts shrinking and the separation falls under 24px, which is where the
     * header stops reading as deliberate; by 840 the customer's own name is
     * clipped. The gap never goes below 8px because a flex floor absorbs the
     * rest — so nothing ever "collides", and waiting for a collision would have
     * meant shipping a truncated dojo name as the signal.
     *
     * Re-derive this if the nav gains an item or the brand lock-up changes.
     * tests/design/nav-breakpoint.spec.ts pins both sides of the transition.
     */
    @media(max-width:900px) {
      body>nav {
        padding: 0 1rem;
        height: 60px;
        /* The dojo's mark sits on the page's centre line on a phone; the
           menu and session controls keep the right edge. Centring by layout
           rather than margin so the mark stays centred whatever the controls
           beside it weigh. */
        justify-content: center;
      }

      body>nav .nav-right {
        position: absolute;
        right: 1rem;
        top: 50%;
        transform: translateY(-50%);
      }

      .nav-brand-text {
        display: none
      }

      /* Hide glass pill on mobile */
      .nav-links {
        position: fixed;
        top: 0;
        left: 0;
        right: 0;
        bottom: 0;
        z-index: var(--z-overlay);
        flex-direction: column;
        align-items: stretch;
        gap: 0;
        background: rgba(4, 9, 20, 0.97);
        backdrop-filter: blur(20px);
        -webkit-backdrop-filter: blur(20px);
        border: none;
        border-radius: 0;
        padding: 0;
        margin: 0;
        box-shadow: none;
        overflow-y: auto;
        -webkit-overflow-scrolling: touch;
        transform: translateY(100%);
        opacity: 0;
        visibility: hidden;
        transition: transform .25s cubic-bezier(0.16, 1, 0.3, 1),
          opacity .2s ease,
          visibility .25s ease
      }

      .nav-links.mobile-open {
        transform: translateY(0);
        opacity: 1;
        visibility: visible
      }

      /* Show close button in mobile menu */
      .nav-pill-close {
        display: flex !important;
        justify-content: flex-end;
        padding: 1.25rem 1.5rem .5rem;
        flex-shrink: 0
      }

      .nav-pill-close button {
        background: rgba(255, 255, 255, 0.06);
        border: 1px solid rgba(255, 255, 255, 0.1);
        border-radius: 50%;
        width: 38px;
        height: 38px;
        display: flex;
        align-items: center;
        justify-content: center;
        cursor: pointer;
        color: rgba(255, 255, 255, 0.6);
        transition: background .2s ease
      }

      .nav-pill-close button:hover {
        background: rgba(255, 255, 255, 0.12)
      }

      /* Show all items in mobile menu */
      .nav-links li {
        display: block !important;
        width: 100%
      }

      .nav-links a {
        display: block;
        padding: 1rem 1.75rem;
        font-size: .88rem;
        letter-spacing: .8px;
        border-radius: 0;
        border-bottom: 1px solid rgba(255, 255, 255, 0.04);
        color: rgba(255, 255, 255, 0.65);
        background: none;
        box-shadow: none;
        transition: background .15s ease, color .15s ease
      }

      .nav-links a:hover {
        background: rgba(255, 255, 255, 0.06)
      }

      /* The drawer marks position with a rule down the left edge rather than a
         pill. Both the rule and the label take the lightness-floored accent,
         because both are the tenant's colour on the dark band. */
      .nav-links a.active {
        color: var(--accent-on-inverse);
        background: color-mix(in oklab, var(--accent) 12%, transparent);
        border-left: 3px solid var(--accent-on-inverse);
        padding-left: calc(1.75rem - 3px);
        box-shadow: none
      }

      /* More items shown inline on mobile */
      .dock-more-wrap {
        display: contents !important
      }

      .dock-more-trigger {
        display: none !important
      }

      .dock-more-dropdown {
        position: static !important;
        opacity: 1 !important;
        visibility: visible !important;
        transform: none !important;
        background: none !important;
        border: none !important;
        box-shadow: none !important;
        padding: 0 !important;
        min-width: unset !important;
        pointer-events: auto !important;
        backdrop-filter: none !important
      }

      .dock-more-dropdown li {
        display: block !important
      }

      #li-admin.admin-hidden,
      #li-tournament-manager.admin-hidden,
      #li-platform-owner.admin-hidden {
        display: none !important
      }

      .dock-more-dropdown a {
        padding: 1rem 1.75rem;
        font-size: .88rem
      }

      /* Mobile auth items — visible by default in mobile menu, hidden via .auth-hidden class */
      .nav-links li.mobile-auth-item {
        display: block !important
      }

      .nav-links li.mobile-auth-item.auth-hidden {
        display: none !important
      }

      .nav-links li.mobile-auth-item a,
      .mobile-auth-item a {
        padding: 1rem 1.75rem;
        font-size: .88rem;
        color: var(--accent-on-inverse) !important;
        font-weight: 700;
        cursor: pointer;
        border-bottom: 1px solid rgba(255, 255, 255, 0.04)
      }

      .mobile-session-badge {
        padding: .75rem 1.75rem;
        font-size: .72rem;
        color: var(--muted);
        border-bottom: 1px solid rgba(255, 255, 255, 0.04)
      }

      /* Hide desktop auth buttons */
      #btn-login,
      #btn-logout,
      #btn-my-profile,
      #session-info {
        display: none !important
      }

      .nav-right {
        gap: 0
      }

      /* Top offset for the fixed nav. Stays on the view, which is what sits
         under the nav. */
      .view.active {
        padding-top: 60px
      }

      /* Bottom clearance for the fixed dock, on the DOCUMENT rather than on the
         view — because the dock floats over the whole page, not over one view.
         Measured on homann at 950px, 2026-08-14: the footer is a SIBLING of
         .view.active, not a child, so the view's own padding cleared nothing
         below it and the dock covered the last 36px of the page — the entire
         "© 2026 Homann Karate Dō — all rights reserved" strip, permanently
         unreachable at full scroll.
         No interactive control was obscured, which is why it survived: nothing
         failed, a line of text was simply never visible. */
      /* THE SAME RULE NOW CLEARS BOTH FIXED OBJECTS.
         AI Sensei docks to a bottom rail at these widths (see clampPlacement in
         ks-ai-sensei-widget.v1.js) and is therefore a second fixed thing
         standing over the end of the page — the identical defect this rule was
         written for, with a different object. The widget publishes its own
         occupied height as --ks-sensei-reserve because only JS can measure a
         figure whose size depends on whether it is tucked; the fallback is 0px,
         so a page that never renders the assistant is unchanged to the pixel,
         and so is one where the script fails to load. */
      body {
        padding-bottom: calc(60px + env(safe-area-inset-bottom, 0px) + var(--ks-sensei-reserve, 0px))
      }

      /* ── Mobile Bottom Dock ──────────────────────────────────────────────── */
      .mobile-dock {
        display: flex;
        position: fixed;
        bottom: 0;
        left: 0;
        right: 0;
        z-index: var(--ks-z-sticky);
        height: calc(60px + env(safe-area-inset-bottom, 0px));
        padding-bottom: env(safe-area-inset-bottom, 0px);
        background: color-mix(in srgb, var(--surface-inverse) 95%, transparent);
        backdrop-filter: blur(16px) saturate(1.3);
        -webkit-backdrop-filter: blur(16px) saturate(1.3);
        border-top: 1px solid rgba(255, 255, 255, 0.10);
        box-shadow: 0 -4px 20px rgba(0, 0, 0, 0.4);
        justify-content: space-around;
        align-items: center
      }

      .dock-btn {
        display: flex;
        flex-direction: column;
        align-items: center;
        justify-content: center;
        gap: 3px;
        background: none;
        border: none;
        color: rgba(255, 255, 255, 0.4);
        font-family: 'Raleway', sans-serif;
        font-size: .55rem;
        font-weight: 700;
        text-transform: uppercase;
        letter-spacing: .5px;
        padding: 6px 10px;
        cursor: pointer;
        text-decoration: none;
        transition: color .15s ease;
        -webkit-tap-highlight-color: transparent
      }

      .dock-btn.active {
        color: var(--accent-on-inverse)
      }

      .dock-btn svg {
        width: 22px;
        height: 22px;
        stroke: currentColor;
        fill: none;
        stroke-width: 1.8;
        stroke-linecap: round;
        stroke-linejoin: round
      }

      .dock-menu-btn.menu-active {
        color: var(--accent-on-inverse)
      }
    }

    /* ── The crowded band ─────────────────────────────────────────────────────
       Between the mobile dock's cut-off and roughly 1340px, a signed-in
       member's three groups want more room than there is. Clipping above means
       that is survivable rather than broken, but an ellipsis on the dojo's name
       is still the worst of the available outcomes -- so the space is taken back
       from spacing first, which costs nothing legible.

       BOUNDS UNCHANGED at 1101. Moving the dock to 900px opened a new 901-1100
       range, and this band was tried there first -- it was the wrong tool.
       Measured at 920px with a signed-in header: even WITH this band's 64px
       logo clamp the dojo name still truncated (gap 9px, .nav-right 242px),
       while for a signed-out visitor the clamp shrank a brand mark that had
       65px of room to spare. Too weak where it was needed and too strong where
       it was not. The 901-1100 range gets its own rule below instead, and this
       band stays exactly as it was tuned.

       It lives here rather than beside .nav-brand because
       dojo-global-command-dock-menu-controls.v1.css defines .nav-links a at the
       same specificity and loads after it -- written there, every one of these
       link rules lost on source order and the band reclaimed nothing at all. */
    @media (min-width: 1101px) and (max-width: 1340px) {

      body>nav {
        padding: 0 .85rem;
      }

      .nav-links {
        gap: 0;
      }

      .nav-links a {
        padding-left: .55rem;
        padding-right: .55rem;
      }

      .nav-right {
        gap: .4rem;
      }

      /* The logo is flex-shrink:0, so it sets the floor the brand cannot go
         below -- at 98px it was that floor, and the name got whatever was left,
         which at the bottom of this band was nothing. Smaller here and the mark
         is still plainly the dojo's. */
      .nav-brand-logo {
        max-width: 64px;
      }

      /* .nav-right never gives ground at any width, which is why the brand was
         absorbing every pixel of the shortfall on its own. In a contest for
         room the dojo's name outranks the viewer's own -- they know who they
         are, and it is the name that says whose site this is -- so the session
         chip yields here, and truncates rather than wrapping when it does. */
      .nav-right {
        flex-shrink: 1;
        min-width: 0;
      }

      #session-info {
        min-width: 0;
      }

      /* Clipping the wrapper cut the pill's own border off mid-curve, which
         reads as a rendering fault rather than as truncation. The badge keeps
         its shape and the role suffix inside it goes instead: of the two things
         it says, which account this is matters more than a label for a role the
         interface already reflects. */
      .session-badge {
        /* inline-block, not the inline-flex it is elsewhere, because
           text-overflow does nothing to a flex container -- the name clipped
           mid-word instead of ellipsing. With the role suffix hidden below the
           badge holds one text node, which is exactly the case inline-block
           handles. */
        display: inline-block;
        min-width: 0;
        max-width: 100%;
        overflow: hidden;
        text-overflow: ellipsis;
        white-space: nowrap;
      }

      .session-badge>span {
        display: none;
      }

      /* The chip is the part that yields; the two controls beside it are the
         way out of the account and stay whole. */
      .nav-right>.btn {
        flex-shrink: 0;
      }
    }

    /* ── The narrow desktop band: 901-1100 ────────────────────────────────────
     *
     * Opened on 2026-08-14 when the mobile dock moved from 1100px to 900px.
     * These 200px are the narrowest the DESKTOP navigation ever has to be, and
     * they did not exist before -- the dock used to cover them.
     *
     * Signed out, the header is already comfortable here: at 920px the brand
     * sits 65px clear of the links with its full mark. Signed IN it is not, and
     * measurement is what settled the fix. At 920px with a session chip the
     * dojo's name TRUNCATED, and it still truncated after borrowing the
     * 1101-1340 band's aggressive 64px logo clamp (gap 9px, .nav-right 242px).
     * The chip is simply too wide for this range.
     *
     * So the chip yields, and nothing else has to. Hiding it at 920px gives a
     * 33px gap with the FULL brand lock-up intact and no truncation anywhere --
     * strictly better than clamping the logo, which cost the mark 34px and
     * still left the name clipped.
     *
     * The precedent is the neighbouring band's own reasoning, applied one step
     * further: "in a contest for room the dojo's name outranks the viewer's own
     * -- they know who they are, and it is the name that says whose site this
     * is". Here there is not enough room to keep both the name and the chip, so
     * the chip goes. Nothing is lost: Log In / My Profile stays, and identity
     * and sign-out remain one click away in that control.
     */
    @media (min-width: 901px) and (max-width: 1100px) {

      /* Spacing reclaimed first, the same way and for the same reason as the
         wider band -- it costs nothing legible.

         .4rem rather than the wider band's .55rem, and the difference is a
         measurement rather than a preference. A SIGNED-IN member's nav is not
         the one this breakpoint was derived against: an admin carries "Match
         Library" as a sixth item, which takes the link row from 402px to 534px.
         At 920px that left the dojo name 12px short even with the session chip
         already gone (title wanted 166px, had 154px). Six items at .15rem a
         side gives ~29px back, which covers it with room to spare and costs
         nothing a reader can see. */
      body>nav {
        padding: 0 .85rem;
      }

      .nav-links {
        gap: 0;
      }

      .nav-links a {
        padding-left: .4rem;
        padding-right: .4rem;
      }

      /* The viewer's own name, and only it. Deliberately NOT the whole
         .nav-right: the auth control beside it is the one thing in this corner
         a signed-out visitor is told to use, and a signed-in one reaches their
         profile through. */
      #session-info {
        display: none !important;
      }
    }


/* ══════════════════════════════════════════════════════════════════════
   dojo-mobile-menu-failsafe-override.v1.css
   ══════════════════════════════════════════════════════════════════════ */
    /* ── Mobile Menu Items – Failsafe Override ────────────────────────────── */
    html.menu-open body>nav {
      z-index: 9999 !important
    }

    .nav-links.mobile-open {
      position: fixed !important;
      top: 0 !important;
      left: 0 !important;
      right: 0 !important;
      bottom: 0 !important;
      width: 100vw !important;
      height: 100vh !important;
      height: 100dvh !important;
      z-index: 9998 !important;
      display: flex !important;
      flex-direction: column !important;
      align-items: stretch !important;
      background: rgba(4, 9, 20, 0.97) !important;
      backdrop-filter: blur(20px) !important;
      -webkit-backdrop-filter: blur(20px) !important;
      overflow-y: auto !important;
      -webkit-overflow-scrolling: touch;
      transform: translateY(0) !important;
      opacity: 1 !important;
      visibility: visible !important
    }

    .nav-links.mobile-open>li {
      display: block !important;
      width: 100%
    }

    .nav-links.mobile-open>li.dock-more-wrap {
      display: contents !important
    }

    .nav-links.mobile-open .dock-more-trigger {
      display: none !important
    }

    .nav-links.mobile-open .dock-more-dropdown {
      position: static !important;
      opacity: 1 !important;
      visibility: visible !important;
      transform: none !important;
      background: none !important;
      border: none !important;
      box-shadow: none !important;
      padding: 0 !important;
      min-width: unset !important;
      pointer-events: auto !important;
      backdrop-filter: none !important
    }

    .nav-links.mobile-open .dock-more-dropdown li {
      display: block !important
    }

    /* THE HIDDEN CONTRACT OUTRANKS THE DRAWER, AS A CLASS CONTRACT — not as a
       list of ids. The show-all rule above ties the hide classes on
       specificity and beat them on source order, and this file then re-hid
       casualties one incident at a time: first the two admin entries (by id),
       then the Team Store (whose id rule here broke its entitlement reveal —
       the note that used to sit below). The third casualty was found in
       production: a signed-out visitor's drawer showed TWO Team entries and a
       Match Library that refuses them, because ks-team-hidden — the newest
       hide class — had no drawer-strength selector. Desktop was always
       correct; only this file's cascade was wrong.

       So the exception is now the CONTRACT: any element a hide class has
       spoken for stays hidden in the drawer, whichever class it is, including
       the next one somebody adds to this list rather than as a new id rule.
       (0,3,1) beats the show rule's (0,2,1) regardless of order. */
    .nav-links.mobile-open>li.ks-team-hidden,
    .nav-links.mobile-open>li.admin-hidden,
    .nav-links.mobile-open>li.auth-hidden,
    .nav-links.mobile-open>li.ks-feature-hidden {
      display: none !important
    }

    /* ══ THE DRAWER, COMPOSED ═══════════════════════════════════════════════
       The failsafe above guarantees the drawer EXISTS whatever the cascade
       does; this block is what it looks like. One long flat list becomes four
       short groups (Explore / Tools / Manage / Account) under small labels,
       on a right-anchored sheet instead of a full-bleed wall — the page stays
       visible behind the overlay, which is what tells a reader where they
       are. Colours stay on shell tokens and the tenant accent variable; no
       tenant's palette is hardcoded here. */
    .nav-links.mobile-open {
      left: auto !important;
      width: min(86vw, 360px) !important;
      border-left: 1px solid rgba(255, 255, 255, .08);
      box-shadow: -24px 0 60px rgba(2, 6, 16, .55);
      padding: 1rem 1.1rem calc(1.25rem + env(safe-area-inset-bottom, 0px));
      gap: 0 !important;
      animation: ks-drawer-in .28s cubic-bezier(.32, .72, .28, 1) both;
    }

    .mobile-menu-overlay.visible {
      animation: ks-overlay-in .28s ease both;
    }

    @keyframes ks-drawer-in {
      from { transform: translateX(28px); opacity: .4; }
      to { transform: translateX(0); opacity: 1; }
    }

    @keyframes ks-overlay-in {
      from { opacity: 0; }
      to { opacity: 1; }
    }

    @media (prefers-reduced-motion: reduce) {
      .nav-links.mobile-open,
      .mobile-menu-overlay.visible {
        animation: none;
      }
    }

    /* Section labels: markup-invisible everywhere except the open drawer and
       the desktop Resources dropdown, where they carry the grouping. */
    .nav-links .drawer-section-label {
      display: none;
      font-size: .64rem;
      font-weight: 800;
      letter-spacing: .14em;
      text-transform: uppercase;
      color: rgba(255, 255, 255, .52);
      padding: 1.1rem .35rem .35rem;
      user-select: none;
    }

    .nav-links.mobile-open .drawer-section-label {
      display: block !important;
    }

    .nav-links.mobile-open .drawer-section-label:first-of-type {
      padding-top: .4rem;
    }

    /* Rows: one quiet geometry, 48px targets, the tenant accent marking the
       active destination with a bar rather than a glow. */
    .nav-links.mobile-open > li a,
    .nav-links.mobile-open .dock-more-dropdown li a {
      display: flex !important;
      align-items: center;
      min-height: 48px;
      padding: .45rem .8rem !important;
      border-radius: 10px;
      font-size: 1rem !important;
      font-weight: 600 !important;
      color: rgba(255, 255, 255, .88) !important;
      text-decoration: none;
    }

    .nav-links.mobile-open li a:active {
      background: rgba(255, 255, 255, .07);
    }

    .nav-links.mobile-open li a.active {
      background: rgba(255, 255, 255, .06);
      color: #fff !important;
      box-shadow: inset 3px 0 0 var(--accent, var(--tn-accent, #1552D8));
    }

    /* The Account group reads as an area, not more menu: the identity badge
       becomes a quiet card, and the actions sit under it. */
    .nav-links.mobile-open .drawer-section-label#drawer-account-label {
      margin-top: auto;
      border-top: 1px solid rgba(255, 255, 255, .08);
      padding-top: 1rem;
    }

    .nav-links.mobile-open .mobile-session-badge {
      border-radius: 10px;
      background: rgba(255, 255, 255, .05);
      border: 1px solid rgba(255, 255, 255, .08);
    }

    /* The close control: one round target in the top corner, nothing else on
       that row. */
    .nav-links.mobile-open .nav-pill-close {
      display: flex !important;
      justify-content: flex-end;
      padding: 0 0 .3rem;
    }

    .nav-links.mobile-open .nav-pill-close button {
      display: grid;
      place-items: center;
      width: 44px;
      height: 44px;
      border-radius: 50%;
      border: 1px solid rgba(255, 255, 255, .14);
      background: rgba(255, 255, 255, .05);
      color: rgba(255, 255, 255, .85);
      cursor: pointer;
    }

    /* ══ THE DESKTOP DROPDOWN, GROUPED ══════════════════════════════════════
       The section labels give the Resources panel its structure — but only
       where a label earns its row. The trigger already says Resources, so a
       Tools header under it repeats the word above it; the drawer keeps that
       label because there the same list sits in one long flat column. Manage
       stays on desktop: it separates management destinations only managers
       ever see from the tools everyone gets. */
    .dock-more-dropdown .drawer-section-label {
      display: block;
      padding: .5rem .85rem .2rem;
      color: var(--ks-ink-3, #6b7a90);
    }

    /* !important because .dock-more-dropdown li carries display:block
       !important in three stylesheets (the same fight the hide classes in
       dojo-global-team-nav-hide.v1.css document); :not(.mobile-open) keeps
       the drawer's own reveal untouched. */
    .nav-links:not(.mobile-open) .dock-more-dropdown .drawer-section-label#drawer-tools-label {
      display: none !important;
    }

    /* Search moved to the action bar on desktop — it opens an overlay and is
       not a destination, so it stopped sitting in a list of pages. The drawer
       keeps it: a phone has no action bar to hold it, and the same
       ksOpenSearch answers either way. Same !important for the same reason as
       the label above: .dock-more-dropdown li carries display:block !important
       in three stylesheets. */
    .nav-links:not(.mobile-open) .dock-more-dropdown li#li-search {
      display: none !important;
    }

    .dock-more-dropdown .drawer-section-label#drawer-manage-label {
      border-top: 1px solid var(--ks-line-soft, rgba(120, 134, 160, .25));
      margin-top: .35rem;
      padding-top: .6rem;
    }


/* ══════════════════════════════════════════════════════════════════════
   dojo-mobile-menu-body-scroll-lock.v1.css
   ══════════════════════════════════════════════════════════════════════ */
    /* ── Body scroll lock when mobile menu is open ──────────────────────────── */
    html.menu-open,
    html.menu-open body {
      overflow: hidden !important;
      position: relative;
      height: 100%
    }


/* ══════════════════════════════════════════════════════════════════════
   dojo-mobile-menu-reduced-motion.v1.css
   ══════════════════════════════════════════════════════════════════════ */
    /* ── Reduced motion ─────────────────────────────────────────────────────── */
    @media (prefers-reduced-motion: reduce) {

      .nav-links,
      .dock-more-dropdown,
      .mobile-menu-overlay,
      .dock-btn {
        transition-duration: 0.01ms !important
      }
    }


/* ══════════════════════════════════════════════════════════════════════
   dojo-phone-only-layout-tweaks.v1.css
   ══════════════════════════════════════════════════════════════════════ */
    /* ── PHONE-ONLY layout tweaks (≤ 640px) ────────────────────────────────── */
    @media(max-width:640px) {
      .fighters-wrap {
        padding: 0 .75rem 3rem
      }

      .grid {
        grid-template-columns: 1fr
      }

      .fcard-photo {
        width: 100px;
        min-width: 100px
      }

      /* The .hiw-step-row rules that sat here were retired on 2026-09-01 with
         the markup they propped up: the steps are a mobile-first grid in
         ks-how-it-works.v1.css and size themselves. */

      .hero {
        padding: 1.5rem 1rem 1rem
      }

      .hero-title {
        letter-spacing: 5px
      }

      /* Old action grid removed — command center handles responsive via cc-* classes */

      .admin-calendar-day {
        min-height: 102px;
        padding: .45rem
      }

      .admin-calendar-grid-wrap {
        padding: .7rem
      }

      .score-card {
        flex-wrap: wrap;
        gap: .6rem
      }

      .score-opp {
        flex-basis: 100%;
        justify-content: flex-start;
        flex-direction: row-reverse
      }

      .grid {
        gap: .75rem
      }

      .ft-search-panel {
        padding: 1rem
      }

      .ft-form {
        padding: 1rem
      }

      .ft-card {
        flex-direction: column
      }

      .ft-card-right {
        flex-direction: row;
        align-items: center;
        gap: .75rem
      }
    }


/* ══════════════════════════════════════════════════════════════════════
   dojo-readability-boost-base.v1.css
   ══════════════════════════════════════════════════════════════════════ */
    /* ── READABILITY BOOST ─────────────────────────────────────────────────── */
    body {
      font-weight: 500
    }


/* ══════════════════════════════════════════════════════════════════════
   dojo-readability-boost-nav.v1.css
   ══════════════════════════════════════════════════════════════════════ */
    /* Nav */
    .nav-title {
      font-size: 1.15rem
    }

    .nav-sub {
      font-size: .72rem
    }

    .nav-links a {
      font-size: .82rem
    }


/* ══════════════════════════════════════════════════════════════════════
   dojo-readability-boost-buttons.v1.css
   ══════════════════════════════════════════════════════════════════════ */
    /* Buttons */
    .btn {
      font-size: .85rem;
      font-weight: 800
    }

    .btn-sm {
      font-size: .78rem
    }

    .hero-btns .btn {
      font-size: .85rem
    }


/* ══════════════════════════════════════════════════════════════════════
   dojo-readability-boost-page-headers.v1.css
   ══════════════════════════════════════════════════════════════════════ */
    /* Page headers */
    .page-hdr h2 {
      font-size: 1.4rem
    }

    .page-hdr p {
      font-size: .88rem;
      font-weight: 500
    }


/* ══════════════════════════════════════════════════════════════════════
   dojo-readability-boost-footer.v1.css
   ══════════════════════════════════════════════════════════════════════ */
    /* Footer */
    footer {
      font-size: .75rem
    }


/* ══════════════════════════════════════════════════════════════════════
   dojo-readability-boost-breadcrumb-back-link.v1.css
   ══════════════════════════════════════════════════════════════════════ */
    /* Breadcrumb & back link */
    .breadcrumb {
      font-size: .78rem
    }

    .back-link {
      font-size: .78rem
    }


/* ══════════════════════════════════════════════════════════════════════
   dojo-readability-boost-form-labels-inputs.v1.css
   ══════════════════════════════════════════════════════════════════════ */
    /* Form labels & inputs */
    .fl {
      font-size: .72rem
    }

    .fi-inp {
      font-size: 1rem
    }

    .search-wrap input {
      font-size: 1rem
    }


/* ══════════════════════════════════════════════════════════════════════
   dojo-readability-boost-fight-items.v1.css
   ══════════════════════════════════════════════════════════════════════ */
    /* Fight items */
    .fi-title {
      font-size: 1.05rem;
      color: #FFFFFF;
    }

    .fi-meta {
      font-size: .85rem;
      color: #E0DCD2;
    }

    .fi-score {
      font-size: 1.05rem;
      color: var(--ks-accent-strong);
    }

    .fi-thumb-label {
      font-size: .75rem;
      color: #E0DCD2;
    }

    .section-label-left {
      font-size: .85rem;
      color: var(--ks-accent-strong);
    }


/* ══════════════════════════════════════════════════════════════════════
   dojo-readability-boost-video-detail.v1.css
   ══════════════════════════════════════════════════════════════════════ */
    /* Video detail */
    .vd-tournament-title {
      font-size: 1.3rem
    }

    .vd-back {
      font-size: .78rem
    }

    .ep-title {
      font-size: 1rem
    }

    .video-placeholder p {
      font-size: .85rem
    }


/* ══════════════════════════════════════════════════════════════════════
   dojo-detail-light-bg-overrides.v1.css
   ══════════════════════════════════════════════════════════════════════ */
    /* ── Tournament Detail — light-bg fight card overrides ─────────────── */
    #view-tournament-detail .fight-item {
      background: #FFF;
      border-color: #E2E8F0;
      box-shadow: 0 1px 3px rgba(15, 23, 42, 0.04), 0 4px 16px rgba(15, 23, 42, 0.05);
    }

    #view-tournament-detail .fi-title {
      color: #0F172A;
    }

    #view-tournament-detail .fi-meta {
      color: #64748B;
    }

    #view-tournament-detail .fight-item:hover {
      border-color: color-mix(in oklab, var(--accent) 30%, transparent);
      box-shadow: 0 8px 28px rgba(15, 23, 42, 0.1), 0 0 0 1px color-mix(in oklab, var(--accent) 6%, transparent);
    }

    #view-tournament-detail .fi-icon {
      background: #F1F5F9;
    }

    #view-tournament-detail .fi-score {
      color: var(--ks-accent-strong);
    }

    #view-tournament-detail .fi-thumb-placeholder {
      background: linear-gradient(135deg, #0B1120, #1A2640);
    }

    #view-tournament-detail .page-light-content.wide .fights-list {
      padding: 0;
    }

    /* ── Fighter Detail — light-bg overrides ───────────────────────────── */
    #view-fighter-detail .edit-panel {
      background: #FFF;
      border-color: #E2E8F0;
      box-shadow: 0 4px 20px rgba(0, 0, 0, 0.04);
      border-radius: 12px;
    }

    #view-fighter-detail .fi-inp {
      background: #F8FAFC;
      border-color: #CBD5E1;
      color: #0F172A;
    }

    #view-fighter-detail .fi-inp:focus {
      border-color: var(--ks-accent);
    }

    #view-fighter-detail .fi-inp::placeholder {
      color: #94A3B8;
    }

    #view-fighter-detail .fl {
      color: #64748B;
    }

    #view-fighter-detail .ep-title {
      color: #0F172A;
    }

    #view-fighter-detail .fd-profile-section {
      border-top-color: #E2E8F0;
    }

    #view-fighter-detail .fd-profile-label {
      color: var(--ks-accent-strong);
    }

    #view-fighter-detail .fd-profile-value {
      color: #374151;
    }

    #view-fighter-detail .fd-profile-inp {
      background: #F8FAFC;
      border-color: #CBD5E1;
      color: #0F172A;
    }

    #view-fighter-detail .fd-profile-inp::placeholder {
      color: #94A3B8;
    }

    #view-fighter-detail .section-label-left {
      color: var(--ks-accent-strong);
    }

    #view-fighter-detail .fight-item {
      background: #FFF;
      border-color: #E2E8F0;
      box-shadow: 0 1px 3px rgba(15, 23, 42, 0.04), 0 4px 16px rgba(15, 23, 42, 0.05);
    }

    #view-fighter-detail .fi-title {
      color: #0F172A;
    }

    #view-fighter-detail .fi-meta {
      color: #64748B;
    }

    #view-fighter-detail .fight-item:hover {
      border-color: color-mix(in oklab, var(--accent) 30%, transparent);
      box-shadow: 0 8px 28px rgba(15, 23, 42, 0.1), 0 0 0 1px color-mix(in oklab, var(--accent) 6%, transparent);
    }

    #view-fighter-detail .fi-icon {
      background: #F1F5F9;
    }

    #view-fighter-detail .fi-score {
      color: var(--ks-accent-strong);
    }

    #view-fighter-detail .fi-thumb-placeholder {
      background: linear-gradient(135deg, #0B1120, #1A2640);
    }

    #view-fighter-detail .page-light-content.wide .fights-list {
      padding: 0;
    }

    #view-fighter-detail .photo-upload-label {
      border-color: color-mix(in oklab, var(--accent) 35%, transparent);
      color: var(--ks-accent-strong);
    }

    #view-fighter-detail .photo-upload-label:hover {
      border-color: var(--ks-accent);
      background: color-mix(in oklab, var(--accent) 8%, transparent);
    }

    #view-fighter-detail .btn-outline.premium-btn-outline {
      border-color: color-mix(in oklab, var(--accent) 35%, transparent);
      color: var(--ks-accent-strong);
    }

    #view-fighter-detail .btn-danger {
      background: rgba(214, 69, 69, 0.08);
    }

    #view-fighter-detail label span {
      color: #475569;
    }


/* ══════════════════════════════════════════════════════════════════════
   dojo-notes-typography.v1.css
   ══════════════════════════════════════════════════════════════════════ */
    /* Notes */
    .notes-container-hdr span {
      font-size: .82rem
    }

    .notes-section-hdr span {
      font-size: .82rem
    }

    .note-text {
      font-size: .92rem;
      font-weight: 500
    }

    .note-time {
      font-size: .82rem
    }

    .note-del {
      font-size: .85rem
    }

    .time-input {
      font-size: .9rem
    }

    .add-note-text {
      font-size: .92rem
    }

    .add-note-btn-wrap .btn {
      font-size: .85rem
    }

    .photo-upload-label {
      font-size: .78rem
    }


/* ══════════════════════════════════════════════════════════════════════
   dojo-unified-page-premium-polish.v1.css
   ══════════════════════════════════════════════════════════════════════ */
    /* ══════════════════════════════════════════════════════════════════
   PREMIUM VISUAL POLISH — Scoped to unified pages
   CSS-only enhancements. No layout or logic changes.
   ══════════════════════════════════════════════════════════════════ */

    /* ── 1. KEYFRAME: Soft fade-in-up ──────────────────────────────── */
    @keyframes polishFadeIn {
      from {
        opacity: 0;
        transform: translateY(8px);
      }

      to {
        opacity: 1;
        transform: translateY(0);
      }
    }

    /* ── 2. NAVY HEADER POLISH ─────────────────────────────────────── */
    .page-navy-header {
      background: linear-gradient(180deg, #020617 0%, #0B1526 100%);
    }

    .page-navy-header::after {
      content: '';
      position: absolute;
      bottom: 0;
      left: 5%;
      right: 5%;
      height: 1px;
      background: linear-gradient(90deg, transparent, color-mix(in oklab, var(--accent) 18%, transparent), transparent);
      pointer-events: none;
    }

    .page-navy-header .page-hdr h2 {
      text-shadow: 0 0 30px color-mix(in oklab, var(--accent) 8%, transparent);
      letter-spacing: 5px;
    }

    .page-navy-header .page-hdr p {
      letter-spacing: 1px;
      margin-top: .35rem;
    }

    /* ── 3. CONTENT SECTION FADE-IN ────────────────────────────────── */
    .page-light-content {
      animation: polishFadeIn .4s cubic-bezier(0.165, 0.84, 0.44, 1) both;
    }

    /* ── 4. FIGHT CARD ENHANCEMENTS ────────────────────────────────── */
    #view-tournament-detail .fight-item,
    #view-fighter-detail .fight-item {
      background: linear-gradient(180deg, #FFFFFF 0%, #FAFBFD 100%);
      border-radius: 12px;
      transition: all .28s cubic-bezier(0.165, 0.84, 0.44, 1);
    }

    #view-tournament-detail .fight-item:hover,
    #view-fighter-detail .fight-item:hover {
      transform: translateY(-3px);
      box-shadow:
        0 12px 36px rgba(15, 23, 42, 0.08),
        0 4px 12px rgba(15, 23, 42, 0.04),
        0 0 0 1px color-mix(in oklab, var(--accent) 10%, transparent);
    }

    #view-tournament-detail .fi-thumb,
    #view-fighter-detail .fi-thumb {
      border-radius: 12px 12px 0 0;
      overflow: hidden;
    }

    /* ── 5. EDIT PANEL GLASS EFFECT ────────────────────────────────── */
    #view-fighter-detail .edit-panel {
      background: linear-gradient(180deg, #FFFFFF 0%, #F8FAFC 100%);
      border: 1px solid rgba(226, 232, 240, 0.8);
      box-shadow:
        0 1px 2px rgba(15, 23, 42, 0.03),
        0 4px 16px rgba(15, 23, 42, 0.04),
        0 12px 40px rgba(15, 23, 42, 0.03);
      transition: box-shadow .3s ease;
    }

    #view-fighter-detail .edit-panel:hover {
      box-shadow:
        0 1px 2px rgba(15, 23, 42, 0.04),
        0 8px 24px rgba(15, 23, 42, 0.06),
        0 16px 48px rgba(15, 23, 42, 0.04);
    }

    /* ── 6. INPUT REFINEMENTS ──────────────────────────────────────── */
    #view-fighter-detail .fi-inp {
      border-radius: var(--ks-r-control);
      transition: border-color .2s ease, box-shadow .2s ease;
    }

    #view-fighter-detail .fi-inp:focus {
      box-shadow: 0 0 0 3px color-mix(in oklab, var(--accent) 10%, transparent);
    }

    #view-fighter-detail .fd-profile-inp {
      border-radius: 8px;
      transition: border-color .2s ease, box-shadow .2s ease;
    }

    #view-fighter-detail .fd-profile-inp:focus {
      border-color: var(--ks-accent);
      box-shadow: 0 0 0 3px color-mix(in oklab, var(--accent) 10%, transparent);
    }

    /* ── 7. BUTTON PREMIUM STATES ──────────────────────────────────── */
    #view-tournament-detail .btn,
    #view-fighter-detail .btn,
    #view-schedule .btn {
      /* Named properties, matching .btn in dojo-global-button-system.v1.css.
         This rule outranks that one, so leaving it as `all` would keep the
         focus ring animating on these three views — and #view-schedule .btn
         is one of the surfaces that paints a visible outline ring. */
      transition-property: background, border-color, color, box-shadow, transform, filter, opacity;
      transition-duration: .22s;
      transition-timing-function: cubic-bezier(0.165, 0.84, 0.44, 1);
    }

    #view-fighter-detail .btn-gold:hover,
    #view-tournament-detail .btn-gold:hover {
      box-shadow: 0 4px 18px color-mix(in oklab, var(--accent) 30%, transparent), 0 0 0 1px color-mix(in oklab, var(--accent) 12%, transparent);
    }

    #view-fighter-detail .btn-gold:active,
    #view-tournament-detail .btn-gold:active {
      transform: translateY(0) scale(0.98);
      box-shadow: 0 2px 8px color-mix(in oklab, var(--accent) 20%, transparent);
    }

    #view-fighter-detail .btn-outline:hover,
    #view-fighter-detail .premium-btn-outline:hover {
      box-shadow: 0 2px 12px color-mix(in oklab, var(--accent) 12%, transparent);
    }

    /* ── 8. SECTION LABEL REFINEMENT ───────────────────────────────── */
    #view-fighter-detail .section-label-left {
      letter-spacing: 4px;
      font-size: .6rem;
      position: relative;
      padding-bottom: .6rem;
    }

    #view-fighter-detail .section-label-left::after {
      content: '';
      position: absolute;
      bottom: 0;
      left: 0;
      width: 32px;
      height: 2px;
      background: var(--ks-accent);
      border-radius: 1px;
    }



/* ══════════════════════════════════════════════════════════════════════
   dojo-profile-sections-fade-polish.v1.css
   ══════════════════════════════════════════════════════════════════════ */
    /* ── 10. PROFILE SECTIONS FADE ─────────────────────────────────── */
    #view-fighter-detail .fd-profile-section {
      animation: polishFadeIn .35s cubic-bezier(0.165, 0.84, 0.44, 1) both;
    }

    #view-fighter-detail .fd-profile-section:nth-child(2) {
      animation-delay: .05s;
    }

    #view-fighter-detail .fd-profile-section:nth-child(3) {
      animation-delay: .1s;
    }

    #view-fighter-detail .fd-profile-section:nth-child(4) {
      animation-delay: .15s;
    }

    #view-fighter-detail .fd-profile-section:nth-child(5) {
      animation-delay: .2s;
    }


/* ══════════════════════════════════════════════════════════════════════
   dojo-breadcrumb-polish.v1.css
   ══════════════════════════════════════════════════════════════════════ */
    /* ── 11. BREADCRUMB POLISH ─────────────────────────────────────── */
    .page-navy-header .breadcrumb {
      padding: .85rem 1.5rem;
      font-size: .62rem;
      letter-spacing: 1.5px;
    }

    .page-navy-header .breadcrumb a {
      transition: color .2s ease;
    }

    .page-navy-header .breadcrumb a:hover {
      color: var(--ks-accent-strong);
      text-shadow: 0 0 12px color-mix(in oklab, var(--accent) 15%, transparent);
    }


/* ══════════════════════════════════════════════════════════════════════
   dojo-photo-upload-refinement.v1.css
   ══════════════════════════════════════════════════════════════════════ */
    /* ── 12. PHOTO UPLOAD REFINEMENT ───────────────────────────────── */
    #view-fighter-detail .photo-upload-label {
      border-radius: 8px;
      transition: all .22s ease;
    }


/* ══════════════════════════════════════════════════════════════════════
   dojo-mobile-touch-polish.v1.css
   ══════════════════════════════════════════════════════════════════════ */
    /* ── 13. MOBILE TOUCH POLISH ───────────────────────────────────── */
    @media (max-width: 768px) {
      .page-navy-header {
        padding: 0 0 1.8rem;
      }

      .page-navy-header .page-hdr {
        padding: 1rem 1.25rem 0;
      }

      .page-navy-header .page-hdr h2 {
        font-size: 1.05rem;
        letter-spacing: 3.5px;
      }

      .page-light-content {
        padding: 0 1rem 2.5rem;
        margin-top: -.5rem;
      }

      #view-tournament-detail .fight-item,
      #view-fighter-detail .fight-item {
        border-radius: 10px;
      }

      #view-tournament-detail .fight-item:hover,
      #view-fighter-detail .fight-item:hover {
        transform: none;
        /* disable lift on mobile — better for touch */
      }

      #view-fighter-detail .edit-panel {
        border-radius: 10px;
        margin: .75rem 0;
      }

      #view-fighter-detail .btn,
      #view-tournament-detail .btn,
      #view-schedule .btn {
        min-height: 44px;
        /* iOS touch target min */
      }

      .page-navy-header .breadcrumb {
        padding: .75rem 1.25rem;
      }
    }


/* ══════════════════════════════════════════════════════════════════════
   dojo-premium-patch-card-base-variants.v1.css
   ══════════════════════════════════════════════════════════════════════ */
    /* ── PREMIUM PATCH CARDS ─────────────────────────────────────────── */
    .patch-card {
      display: flex;
      align-items: stretch;
      border-radius: 14px;
      overflow: hidden;
      text-decoration: none;
      transition: all .28s cubic-bezier(.4, 0, .2, 1);
      box-shadow: 0 4px 24px rgba(0, 0, 0, 0.05), 0 1px 4px rgba(0, 0, 0, 0.04);
      border: 1px solid rgba(226, 232, 240, 0.6)
    }

    .patch-card:hover {
      transform: translateY(-5px);
      box-shadow: 0 20px 50px rgba(0, 0, 0, 0.1), 0 4px 12px rgba(0, 0, 0, 0.06)
    }

    .patch-card-logo {
      width: 120px;
      display: flex;
      align-items: center;
      justify-content: center;
      flex-shrink: 0;
      padding: 1.5rem;
      position: relative
    }

    .patch-card-logo::after {
      content: '';
      position: absolute;
      top: 15%;
      right: 0;
      bottom: 15%;
      width: 1px;
      background: linear-gradient(180deg, transparent, rgba(255, 255, 255, 0.15), transparent)
    }

    .patch-card-logo img {
      width: 58px;
      height: auto;
      object-fit: contain;
      filter: drop-shadow(0 3px 10px rgba(0, 0, 0, 0.25));
      transition: transform .3s
    }

    .patch-card:hover .patch-card-logo img {
      transform: scale(1.08)
    }

    .patch-card-body {
      flex: 1;
      padding: 1.4rem 1.5rem 1.4rem 1.25rem;
      display: flex;
      flex-direction: column;
      justify-content: center;
      gap: .2rem;
      background: #FFF
    }

    .patch-card-org {
      font-size: .5rem;
      font-weight: 800;
      letter-spacing: 2.5px;
      text-transform: uppercase
    }

    .patch-card-name {
      font-family: var(--ks-display);
      font-size: .95rem;
      font-weight: 700;
      color: #0F172A;
      letter-spacing: .5px;
      line-height: 1.2
    }

    .patch-card-desc {
      font-size: .76rem;
      color: #64748B;
      font-weight: 500;
      line-height: 1.45;
      margin-top: .15rem
    }

    .patch-card-cta {
      display: inline-flex;
      align-items: center;
      gap: .5rem;
      font-size: .58rem;
      font-weight: 800;
      letter-spacing: 1.5px;
      text-transform: uppercase;
      margin-top: .65rem;
      transition: gap .25s
    }

    .patch-card:hover .patch-card-cta {
      gap: .8rem
    }

    .patch-card-arrow {
      width: 26px;
      height: 26px;
      border-radius: 50%;
      display: flex;
      align-items: center;
      justify-content: center;
      flex-shrink: 0;
      font-size: .7rem;
      font-weight: 700;
      transition: all .28s
    }

    .patch-card:hover .patch-card-arrow {
      transform: translateX(3px)
    }
    /* AAU Patch Variant */
    .patch-card.patch-aau .patch-card-logo {
      background: linear-gradient(155deg, #001A5E 0%, #002B8A 100%)
    }

    .patch-card.patch-aau .patch-card-org {
      color: #BF0A30
    }

    .patch-card.patch-aau .patch-card-cta {
      color: #001A5E
    }

    .patch-card.patch-aau .patch-card-arrow {
      background: #001A5E;
      color: #FFF
    }

    .patch-card.patch-aau:hover {
      border-color: rgba(191, 10, 48, 0.2)
    }

    .patch-card.patch-aau:hover .patch-card-arrow {
      background: #BF0A30
    }
    /* NKF Patch Variant */
    .patch-card.patch-nkf .patch-card-logo {
      background: linear-gradient(155deg, #1A1A2E 0%, #0F3460 100%)
    }

    .patch-card.patch-nkf .patch-card-org {
      color: #0033A0
    }

    .patch-card.patch-nkf .patch-card-cta {
      color: #0F3460
    }

    .patch-card.patch-nkf .patch-card-arrow {
      background: #0033A0;
      color: #FFF
    }

    .patch-card.patch-nkf:hover {
      border-color: rgba(0, 51, 160, 0.2)
    }

    .patch-card.patch-nkf:hover .patch-card-arrow {
      background: #002880
    }


/* ══════════════════════════════════════════════════════════════════════
   dojo-tournament-info-responsive-touch.v1.css
   ══════════════════════════════════════════════════════════════════════ */
    @media(max-width: 900px) {
      .ti-hero-inner {
        flex-direction: column;
        text-align: center;
      }

      .ti-coach-staff {
        display: inline-block;
        text-align: center;
      }

      .ti-hero-left h1 {
        font-size: 2.8rem;
      }

      .ti-grid-2 {
        grid-template-columns: 1fr;
      }

      .ti-bottom-banner {
        flex-direction: column;
        text-align: center;
        gap: 1.5rem;
      }

      .ti-bb-left {
        flex-direction: column;
        align-items: center;
        text-align: center;
      }

      .patch-card-logo {
        width: 90px;
        padding: 1.15rem
      }

      .patch-card-logo img {
        width: 46px
      }
    }

    /* ── TI: Touch device safety ──────────────────────────────────── */
    @media (hover: none) {
      .event-card:hover {
        transform: none;
      }

      .ti-card:hover {
        transform: none;
      }

      .patch-card:hover {
        transform: none;
      }

      .ti-bb-btn:hover {
        transform: none;
      }

      .event-card.card-next {
        animation: none;
      }
    }


/* ══════════════════════════════════════════════════════════════════════
   dojo-kumite-rules-premium-polish.v1.css
   ══════════════════════════════════════════════════════════════════════ */
    /* ══════════════════════════════════════════════════════════════════
       KUMITE RULES — PREMIUM POLISH (DARK PAGE + LIGHT TABLE)
       Scoped ONLY to #view-rules. Matches tournament-info palette.
       ══════════════════════════════════════════════════════════════════ */

    /* ─── PAGE & SECTION BACKGROUNDS — shared canvas + dark islands ──
       The dark surfaces below sit on the tenant's own band shades
       (--ks-band-*), the same derived palette the pagehead band above them
       wears — so the matrix stays an intentional dark feature surface and
       still belongs to whichever dojo is rendering it. The gold/blue pair is
       the AAU/NKF semantic and deliberately stays fixed. --rl-band-lift is
       one local derivation: the band raised a step further, for the card
       bodies and hovered rows that used to hardcode their own navy. */
    #view-rules {
      background: var(--ks-canvas) !important;
      --rl-band-lift: color-mix(in oklab, var(--ks-band-raised) 82%, #FFFFFF);
    }

    #view-rules .rl-filter-bar {
      background: transparent !important;
    }

    /* ─── ORG CARD BODIES — the tenant band, dark feature surface ─── */
    #view-rules .rl-org-card.aau {
      background: linear-gradient(145deg, var(--ks-band-deep), var(--ks-band)) !important;
      border: 1px solid rgba(201, 168, 76, .18) !important;
      border-radius: var(--ks-r-lg) !important;
      box-shadow: 0 18px 50px rgba(0, 0, 0, .35), 0 0 0 1px rgba(0, 0, 0, .12) !important;
    }

    #view-rules .aau-card-body {
      background: linear-gradient(145deg, var(--rl-band-lift), color-mix(in oklab, var(--rl-band-lift) 92%, #FFFFFF)) !important;
      border-radius: 0 0 var(--ks-r-lg) var(--ks-r-lg) !important;
    }

    #view-rules .aau-stat-wrap {
      border-bottom-color: rgba(201, 168, 76, .15) !important;
    }

    #view-rules .aau-big-num {
      color: #C9A84C !important;
    }

    #view-rules .aau-big-lbl {
      color: var(--ks-band-ink-3) !important;
    }

    #view-rules .aau-feats li {
      color: var(--ks-band-ink-2) !important;
    }

    #view-rules .rl-org-card.aau .rl-card-btn {
      background: linear-gradient(135deg, #C9A84C, #A58432) !important;
      color: #0A0A0A !important;
      border: 1px solid rgba(201, 168, 76, .5) !important;
    }

    #view-rules .rl-org-card.nkf {
      background: linear-gradient(145deg, var(--ks-band-deep), var(--ks-band)) !important;
      border: 1px solid rgba(100, 181, 246, .15) !important;
      border-radius: var(--ks-r-lg) !important;
      box-shadow: 0 18px 50px rgba(0, 0, 0, .35), 0 0 0 1px rgba(0, 0, 0, .12) !important;
    }

    #view-rules .nkf-card-body {
      background: linear-gradient(145deg, var(--rl-band-lift), color-mix(in oklab, var(--rl-band-lift) 92%, #FFFFFF)) !important;
      border-radius: 0 0 var(--ks-r-lg) var(--ks-r-lg) !important;
    }

    #view-rules .nkf-stat-wrap {
      border-bottom-color: rgba(100, 181, 246, .15) !important;
    }

    #view-rules .nkf-big-num {
      color: #64B5F6 !important;
    }

    #view-rules .nkf-big-lbl {
      color: var(--ks-band-ink-3) !important;
    }

    #view-rules .nkf-feats li {
      color: var(--ks-band-ink-2) !important;
    }

    #view-rules .rl-org-card.nkf .rl-card-btn {
      background: linear-gradient(135deg, #2980B9, #1B6FA0) !important;
      color: #FFF !important;
      border: 1px solid rgba(100, 181, 246, .35) !important;
    }

    /* ─── FILTER TABS — the canvas's own surface and ink ────────── */
    #view-rules .rl-filter-btn {
      background: var(--ks-surface) !important;
      border: 1px solid var(--ks-line) !important;
      color: var(--ks-ink-3) !important;
      box-shadow: 0 2px 6px rgba(0, 0, 0, .04) !important;
    }

    #view-rules .rl-filter-btn.active {
      background: var(--ks-ink) !important;
      color: var(--ks-surface) !important;
      border-color: var(--ks-ink) !important;
      box-shadow: 0 4px 14px rgba(15, 23, 42, .16) !important;
    }

    #view-rules .rl-filter-btn:hover:not(.active) {
      background: var(--ks-surface) !important;
      border-color: var(--ks-accent) !important;
      color: var(--ks-ink) !important;
      box-shadow: 0 3px 10px color-mix(in oklab, var(--accent) 10%, transparent) !important;
    }

    /* ─── 1. TABLE — DARK READABLE ────────────────────────────── */
    #view-rules .rl-table {
      border-collapse: separate !important;
      border-spacing: 0 !important;
    }

    #view-rules .rl-table-wrap {
      background: linear-gradient(145deg, var(--ks-band), var(--ks-band-raised)) !important;
      border: 1px solid var(--ks-band-line) !important;
      border-radius: var(--ks-r-lg) !important;
      overflow: hidden !important;
      box-shadow: 0 12px 40px rgba(0, 0, 0, .3), 0 0 0 1px rgba(0, 0, 0, .1) !important;
    }

    /* ─── 2. ROWS — dark with subtle alternation ─────────────── */
    #view-rules .rl-tr {
      background: color-mix(in oklab, var(--ks-band) 50%, transparent) !important;
      transition: all .2s ease;
      cursor: pointer;
    }

    #view-rules .rl-tr:nth-child(even) {
      background: color-mix(in oklab, var(--ks-band-raised) 60%, transparent) !important;
    }

    #view-rules .rl-td {
      border-bottom: 1px solid var(--ks-band-line) !important;
      color: var(--ks-band-ink-2) !important;
    }

    #view-rules .rl-td:nth-child(1) {
      border-right: 1px solid var(--ks-band-line) !important;
    }

    #view-rules .rl-td:nth-child(2) {
      border-right: 1px solid var(--ks-band-line) !important;
    }

    /* ─── 3. TABLE HEADER ─────────────────────────────────────── */
    #view-rules .rl-table thead th {
      padding: 1.25rem 2rem !important;
      font-size: .72rem !important;
      letter-spacing: 1.5px !important;
    }

    #view-rules .rl-table thead th:nth-child(1) {
      background: linear-gradient(180deg, var(--ks-band), var(--ks-band-raised)) !important;
      color: var(--ks-band-ink) !important;
    }

    #view-rules .rl-table thead th:nth-child(2) {
      background: linear-gradient(135deg, #D4AF37, #B8962E) !important;
      color: #1A0F00 !important;
    }

    #view-rules .rl-table thead th:nth-child(3) {
      background: linear-gradient(135deg, #2B6CB0, #1E4E8C) !important;
      color: rgba(255, 255, 255, .95) !important;
    }

    #view-rules .rl-table thead {
      box-shadow: 0 3px 12px rgba(0, 0, 0, .2);
      position: relative;
      z-index: 1;
    }

    /* ─── 4. ROW HOVER ────────────────────────────────────────── */
    #view-rules .rl-tr:hover {
      background: color-mix(in oklab, var(--rl-band-lift) 70%, transparent) !important;
    }

    #view-rules .rl-tr:focus-visible,
    #view-rules .rl-rc-header:focus-visible {
      outline: 2px solid var(--ks-band-accent);
      outline-offset: -2px;
    }

    #view-rules .rl-tr:active {
      transform: translateY(0);
    }

    /* ─── 5. EXPANDED ROWS ────────────────────────────────────── */
    #view-rules .rl-expand-row {
      background: color-mix(in oklab, var(--ks-band-deep) 70%, transparent) !important;
      border-bottom: 1px solid var(--ks-band-line) !important;
    }

    #view-rules .rl-expand-grid {
      padding: 1.75rem 2.25rem !important;
    }

    #view-rules .rl-expand-row .rl-expand-cell {
      box-shadow: inset 0 2px 6px rgba(0, 0, 0, .1) !important;
    }

    /* #view-rules is a light view with --ks-accent-strong pinned to
       --accent-on-surface, and these two blocks are dark cards sitting inside
       it. The <strong> runs in them measured 2.17-2.24:1 on their own
       backgrounds. Pinned on the containers so every accent-coloured thing
       inside them is right, not just the runs found today.

       (This cited the how-it-works step cards as the same shape. They are no
       longer dark cards — that view moved to .ks-card--inverse on 2026-09-01,
       which is the pin-free way out of this and the better model to copy if
       these two blocks are ever revisited.) */
    #view-rules .rl-expand-col,
    #view-rules .rl-rc-detail-block {
      --ks-accent-strong: var(--accent-on-inverse);
    }

    #view-rules .rl-expand-col strong {
      font-size: .72rem !important;
      letter-spacing: 1.5px !important;
      color: var(--ks-accent-strong) !important;
      padding-bottom: .6rem !important;
      margin-bottom: .85rem !important;
      border-bottom: 2px solid color-mix(in oklab, var(--accent) 15%, transparent) !important;
    }

    #view-rules .rl-expand-col p {
      color: var(--ks-band-ink-2) !important;
      font-size: .92rem !important;
      line-height: 1.75 !important;
    }

    #view-rules .rl-tr.expanded .rl-row-chevron {
      color: var(--ks-accent-strong) !important;
    }

    #view-rules .rl-tr.expanded {
      background: color-mix(in oklab, var(--rl-band-lift) 45%, transparent) !important;
      box-shadow: inset 0 -2px 0 color-mix(in oklab, var(--accent) 15%, transparent);
    }

    /* ─── 6. BADGES ───────────────────────────────────────────── */
    #view-rules .rl-badge.same {
      background: rgba(34, 197, 94, .15) !important;
      color: #4ADE80 !important;
      border: 1px solid rgba(74, 222, 128, .25) !important;
      border-radius: var(--ks-r-pill) !important;
      padding: 4px 10px !important;
      font-weight: 700 !important;
      font-size: .62rem !important;
      letter-spacing: 1.2px !important;
    }

    #view-rules .rl-badge.differs {
      background: rgba(168, 85, 247, .15) !important;
      /* #C084FC measured 4.17:1 against the navy panel at this size -- under 4.5
         for 9.92px text. Lightened one step to clear it. The hue is unchanged, so
         the badge still reads as the same "these two rulesets differ" purple it
         has always been; only its lightness moved. */
      color: #D3AEFD !important;
      border: 1px solid rgba(192, 132, 252, .25) !important;
      border-radius: var(--ks-r-pill) !important;
      padding: 4px 10px !important;
      font-weight: 700 !important;
      font-size: .62rem !important;
      letter-spacing: 1.2px !important;
    }

    /* ─── 7. CATEGORY ICON + TEXT ─────────────────────────────── */
    #view-rules .rl-cat-icon {
      background: color-mix(in oklab, var(--accent) 8%, transparent) !important;
      border: 1px solid color-mix(in oklab, var(--accent) 15%, transparent) !important;
      box-shadow: 0 2px 6px rgba(0, 0, 0, .1);
    }

    #view-rules .rl-cat-text-wrap strong {
      font-weight: 700 !important;
      font-size: 1rem !important;
      color: var(--ks-band-ink) !important;
      letter-spacing: -.2px !important;
    }

    #view-rules .rl-cat-text-wrap span {
      color: var(--ks-band-ink-3) !important;
      font-size: .78rem !important;
    }

    #view-rules .rl-cat-cell {
      gap: 1.25rem !important;
    }

    #view-rules .rl-val-text {
      color: var(--ks-band-ink-2) !important;
    }

    /* ─── MOBILE RULE CARDS — dark ────────────────────────────── */
    #view-rules .rl-rule-card {
      background: linear-gradient(145deg, var(--ks-band), var(--ks-band-raised)) !important;
      box-shadow: 0 8px 24px rgba(0, 0, 0, .25) !important;
      border: 1px solid var(--ks-band-line) !important;
      transition: all .25s ease;
    }

    #view-rules .rl-rule-card.expanded {
      border-color: color-mix(in oklab, var(--accent) 25%, transparent) !important;
      box-shadow: 0 12px 36px rgba(0, 0, 0, .35) !important;
    }

    #view-rules .rl-rule-card.expanded .rl-rc-chevron {
      color: var(--ks-accent-strong) !important;
    }

    #view-rules .rl-rc-detail-block {
      background: color-mix(in oklab, var(--ks-band-deep) 50%, transparent) !important;
      border: 1px solid var(--ks-band-line) !important;
      box-shadow: inset 0 1px 3px rgba(0, 0, 0, .1);
    }

    #view-rules .rl-rc-detail-block strong {
      color: var(--ks-accent-strong) !important;
      border-bottom: 2px solid color-mix(in oklab, var(--accent) 15%, transparent) !important;
    }

    #view-rules .rl-rc-detail-block p {
      color: var(--ks-band-ink-2) !important;
    }

    /* ─── MOBILE CARD TEXT — readable on dark backgrounds ──────── */
    #view-rules .rl-rc-category {
      color: var(--ks-band-ink) !important;
    }

    #view-rules .rl-rc-sub {
      color: var(--ks-band-ink-3) !important;
    }

    #view-rules .rl-rc-val-text {
      color: var(--ks-band-ink-2) !important;
    }

    #view-rules .rl-rc-val-row {
      border-top-color: var(--ks-band-line) !important;
    }

    #view-rules .rl-rc-val-label.aau-label {
      color: #C9A84C !important;
    }

    #view-rules .rl-rc-val-label.nkf-label {
      color: #64B5F6 !important;
    }

    #view-rules .rl-rc-detail-block strong .rl-rc-dot.nkf-dot {
      background: #64B5F6 !important;
    }

    #view-rules .rl-rc-detail-block strong .rl-rc-dot.aau-dot {
      background: #C9A84C !important;
    }

    #view-rules .rl-rc-summary {
      border-top: 1px solid var(--ks-band-line);
    }

    /* ─── REDUCED MOTION ───────────────────────────────────────── */
    @media (prefers-reduced-motion: reduce) {

      #view-rules .rl-org-card.aau,
      #view-rules .rl-org-card.nkf,
      #view-rules .rl-filter-btn,
      #view-rules .rl-tr,
      #view-rules .rl-rule-card,
      #view-rules .rl-card-btn {
        transition: none !important;
      }
    }

    /* ══════════════════════════════════════════════════════════════════
       END KUMITE RULES POLISH
       ══════════════════════════════════════════════════════════════════ */


/* ══════════════════════════════════════════════════════════════════════
   dojo-competitor-athlete-timeline.v1.css
   ══════════════════════════════════════════════════════════════════════ */
    /* ══════════════════════════════════════════════════════════════════════════
       ATHLETE TIMELINE — one entry per tournament, newest first.

       An ordered list with the rail drawn on the entries rather than on a
       separate element, so a screen reader gets a numbered list of events and a
       sighted reader gets the line. The markers are aria-hidden for the same
       reason: they are the line, not information.
       ══════════════════════════════════════════════════════════════════════════ */

    .ks-timeline {
      list-style: none;
      margin: .75rem 0 0;
      padding: 0;
    }

    .ks-timeline-entry {
      position: relative;
      display: flex;
      gap: .9rem;
      padding: 0 0 1.15rem 0;
    }

    /* The rail. Drawn on the marker column so it starts and stops with the
       entries, and dropped on the last one so the line does not trail off. */
    .ks-timeline-entry::before {
      content: '';
      position: absolute;
      left: 7px;
      top: 1.1rem;
      bottom: 0;
      width: 2px;
      background: var(--ks-line);
    }

    .ks-timeline-entry:last-child::before {
      display: none;
    }

    .ks-timeline-marker {
      position: relative;
      z-index: 1;
      flex: 0 0 auto;
      width: 16px;
      height: 16px;
      margin-top: .28rem;
      border-radius: 50%;
      background: var(--ks-surface);
      border: 2px solid var(--ks-line);
    }

    .ks-timeline-body {
      flex: 1 1 auto;
      min-width: 0;
    }

    .ks-timeline-date {
      color: var(--ks-ink-3);
      font-size: .68rem;
      font-weight: 800;
      letter-spacing: .08em;
      text-transform: uppercase;
    }

    .ks-timeline-title {
      color: var(--ks-ink);
      font-weight: 800;
      font-size: .92rem;
      margin-top: .1rem;
      /* Tournament names are free text. */
      overflow-wrap: anywhere;
    }

    .ks-timeline-facts {
      display: flex;
      flex-wrap: wrap;
      gap: .4rem;
      margin-top: .45rem;
    }

    /* A podium finish colours its marker, so the shape of a career reads down
       the rail at a glance. The placement is also written in the chip beside
       it -- the colour repeats that, it never carries it alone. */
    .ks-timeline-entry[data-medal="1"] .ks-timeline-marker {
      background: var(--ks-medal-1-bg);
      border-color: var(--ks-medal-1-line);
    }

    .ks-timeline-entry[data-medal="2"] .ks-timeline-marker {
      background: var(--ks-medal-2-bg);
      border-color: var(--ks-medal-2-line);
    }

    .ks-timeline-entry[data-medal="3"] .ks-timeline-marker {
      background: var(--ks-medal-3-bg);
      border-color: var(--ks-medal-3-line);
    }

    .ks-timeline-record {
      font-variant-numeric: tabular-nums;
    }


/* ══════════════════════════════════════════════════════════════════════
   dojo-competitor-match-discipline.v1.css
   ══════════════════════════════════════════════════════════════════════ */
    /* ══════════════════════════════════════════════════════════════════════════
       MATCH DISCIPLINE — choosing kata or kumite, and reading the result.

       The chooser is a radiogroup of two buttons rather than a select: it is a
       two-way choice that changes which fields below it apply, so it is worth
       the space to show both options and what each one means.
       ══════════════════════════════════════════════════════════════════════════ */

    .ks-discipline {
      display: grid;
      grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
      gap: .5rem;
    }

    .ks-discipline-opt {
      display: flex;
      flex-direction: column;
      gap: .15rem;
      text-align: left;
      padding: .6rem .75rem;
      border-radius: var(--ks-r-md);
      /* Its home, the match modal, wears the light surface now. */
      border: 1px solid var(--ks-line-strong);
      background: var(--ks-surface);
      color: var(--ks-ink);
      cursor: pointer;
      font: inherit;
      transition: border-color .15s ease, background .15s ease;
    }

    .ks-discipline-opt:hover {
      border-color: var(--ks-accent);
    }

    .ks-discipline-opt.is-selected {
      border-color: var(--ks-accent);
      background: var(--ks-accent-soft);
    }

    .ks-discipline-opt:focus-visible {
      outline: 2px solid var(--ks-accent);
      outline-offset: 2px;
    }

    .ks-discipline-name {
      font-weight: 800;
      font-size: .78rem;
      letter-spacing: .04em;
    }

    .ks-discipline-hint {
      font-size: .68rem;
      color: var(--ks-ink-3);
      line-height: 1.35;
    }

    /* A kata card carries this instead of WIN/TIE/LOSS. Neutral on purpose:
       there is no result to colour, and tinting it like a win or a loss would
       be asserting an outcome the record does not hold. */
    .score-badge.badge-kata {
      background: var(--ks-surface-sunken);
      border: 1px solid var(--ks-line);
      color: var(--ks-ink-2);
    }

    /* ── Matches grouped by tournament ──────────────────────────────────────── */
    /* .fights-list is a grid of cards. Grouped, the sections stack and each
       discipline's cards get their own grid inside the section -- otherwise the
       tournaments themselves lay out as columns. */
    .fights-list.is-grouped {
      display: block;
    }

    .fd-fight-grid {
      display: grid;
      grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
      gap: 1rem;
      padding: 0 1.5rem;
    }

    .fd-fight-group {
      margin-bottom: 1.6rem;
    }

    .fd-fight-group-head {
      display: flex;
      align-items: baseline;
      justify-content: space-between;
      gap: .75rem;
      padding: 0 1.5rem .5rem;
      border-bottom: 1px solid var(--ks-line);
      margin-bottom: .65rem;
    }

    .fd-fight-group-title {
      font-family: var(--ks-display);
      font-size: .95rem;
      font-weight: 700;
      letter-spacing: .04em;
      color: var(--ks-ink);
      /* Tournament names are free text. */
      overflow-wrap: anywhere;
    }

    .fd-fight-group-date {
      flex: 0 0 auto;
      font-size: .62rem;
      font-weight: 800;
      letter-spacing: .1em;
      text-transform: uppercase;
      color: var(--ks-ink-3);
    }

    /* Only drawn when a tournament holds both, so a competitor who fought only
       kumite is not made to read a heading telling them so. */
    .fd-fight-discipline-label {
      padding: .35rem 1.5rem .25rem;
      font-size: .6rem;
      font-weight: 800;
      letter-spacing: .14em;
      text-transform: uppercase;
      color: var(--ks-accent-strong);
    }

    /* ── Changing a match ── the app surface's own buttons carry the row;
       the destructive one is tinted from the danger token, never a literal. */
    .vd-match-actions {
      display: flex;
      flex-wrap: wrap;
      gap: .5rem;
      padding: .75rem 1.15rem;
      border-bottom: 1px solid var(--ks-line-soft);
    }

    .ks-btn.vd-delete-match {
      border-color: color-mix(in oklab, var(--ks-danger) 40%, transparent);
      color: var(--ks-danger);
    }

    .ks-btn.vd-delete-match:hover {
      border-color: var(--ks-danger);
      background: var(--ks-danger-bg, var(--ks-surface-sunken));
    }



/* ══════════════════════════════════════════════════════════════════════
   dojo-videos-mobile-responsive.v1.css
   ══════════════════════════════════════════════════════════════════════ */
    /* ─── MOBILE SAFETY ─────────────────────────────────────────── */
    @media (max-width: 800px) {
      #view-tournaments .ti-section {
        border-radius: 14px;
        padding: 20px;
        margin-left: -2px;
        margin-right: -2px;
      }

      #view-tournaments #tournaments-grid .fcard {
        border-radius: 14px !important;
      }

      #view-tournaments #tournaments-grid .fcard:hover {
        transform: none;
      }

      #view-tournaments .grid {
        grid-template-columns: 1fr;
        gap: .75rem;
      }

      #view-tournaments #tournaments-grid .fcard-foot .btn-gold,
      #view-tournaments #tournaments-grid .fcard-foot .btn.btn-gold {
        width: 100%;
        text-align: center;
        justify-content: center;
      }
    }

    @media (max-width: 480px) {
      #view-tournaments .ti-section {
        padding: 16px;
        border-radius: 12px;
      }

      #view-tournaments .ti-section-hdr h3 {
        font-size: 15px !important;
        letter-spacing: 1px;
      }

      #view-tournaments .search-wrap input {
        font-size: .85rem;
      }
    }



/* ══════════════════════════════════════════════════════════════════════
   dojo-unified-route-headers.v1.css
   ══════════════════════════════════════════════════════════════════════ */
    /* ══════════════════════════════════════════════════════════════════
       Unified route headers
       Keeps every main page band on the same navy system without
       touching cards, forms, data rendering, or Firebase logic.
       ══════════════════════════════════════════════════════════════════ */
    :root {
      --ks-route-header-bg: #020817;
      --ks-route-header-bg-2: #0A162A;
      --ks-route-header-height: 280px;
      /* --ks-route-header-gold-rule was declared here and read by nothing.
         Removed rather than renamed: a token with no consumers is not a seam,
         it is a value waiting to be believed. */
    }

    /* The dock is the platform's one dark band, so it asks for the semantic
       inverse surface rather than naming a colour. #020817 was very nearly
       black; --surface-inverse is the brand navy, which is both on-palette and
       light enough to read as a bar sitting ABOVE the page rather than a hole
       cut in it. The hairline goes neutral: a gold rule is the legacy theme
       signing its name, and the tenant's colour belongs on the active pill
       where it means something. */
    body>nav {
      background: color-mix(in srgb, var(--surface-inverse) 96%, transparent);
      border-bottom-color: rgba(255, 255, 255, 0.09);
    }

    /* How It Works left this list on 2026-09-01. Its header is now a real
       `.ks-band .ks-pagehead`, which brings this treatment with it and pays
       its own nav allowance — and the positional `>div:first-child` that
       reached it from here would have re-attached to whatever element
       happened to be first after any future edit. */
    .hero,
    .ti-hero-wrap,
    .page-navy-header,
    #view-rules .rl-hero-bg-container {
      background:
        radial-gradient(ellipse at 50% 0%, color-mix(in oklab, var(--accent) 6%, transparent) 0%, transparent 62%),
        linear-gradient(180deg, var(--ks-route-header-bg) 0%, var(--ks-route-header-bg-2) 100%) !important;
      min-height: var(--ks-route-header-height);
      box-sizing: border-box;
    }

    .hero {
      justify-content: center;
    }

    .ti-hero-wrap {
      display: flex;
      align-items: center;
      padding: 2.35rem 1.5rem 4.25rem !important;
      margin-bottom: -2rem !important;
    }

    .ti-hero-inner {
      width: 100%;
    }

    .ti-hero-left h1,
    .rl-hero-h1 {
      font-size: clamp(2.15rem, 4vw, 3.15rem) !important;
      letter-spacing: 1px;
    }

    .ti-hero-left p,
    .rl-hero-desc {
      margin-bottom: 0 !important;
    }

    .ti-hero-logo {
      max-width: min(280px, 24vw);
      max-height: 132px;
      object-fit: contain;
    }

    .page-navy-header {
      display: flex;
      flex-direction: column;
      justify-content: flex-start;
      padding: 0 0 2.25rem !important;
    }

    .page-navy-header .breadcrumb {
      width: 100%;
    }

    .page-navy-header .page-hdr {
      padding-top: 1.7rem !important;
    }

    #view-rules .rl-hero-bg-container {
      display: flex;
      align-items: center;
      padding-top: 0 !important;
      margin-bottom: 0 !important;
    }

    #view-rules .rl-hero-wrap {
      width: 100%;
      padding: 2.35rem 1.5rem !important;
    }

    #view-rules .rl-hero-text {
      margin-bottom: 0 !important;
    }

    #view-rules .rl-org-cards {
      max-width: var(--ks-measure);
      /* A band ends where it ends: the -1.75rem tuck reached up into a hero
         this page no longer has, pulling the cards across the shared band's
         edge. Content now starts below the band on the shared measure and
         gutter, like every other page. */
      margin: clamp(1.5rem, 4vw, 2.5rem) auto 2.5rem;
      padding: 0 var(--ks-gutter);
    }


/* ══════════════════════════════════════════════════════════════════════
   dojo-unified-route-headers-responsive.v1.css
   ══════════════════════════════════════════════════════════════════════ */
    @media (max-width: 900px) {
      :root {
        --ks-route-header-height: 240px;
      }

      .ti-hero-wrap {
        padding: 2rem 1rem 3.5rem !important;
      }

      .ti-hero-logo {
        max-width: min(220px, 64vw);
        max-height: 110px;
      }

      #view-rules .rl-org-cards {
        padding: 0 var(--ks-gutter);
      }
    }

    @media (max-width: 640px) {
      :root {
        --ks-route-header-height: 220px;
      }

      .ti-hero-left h1,
      .rl-hero-h1 {
        font-size: clamp(1.8rem, 10vw, 2.35rem) !important;
      }

      .page-navy-header .page-hdr {
        padding-top: 1.15rem !important;
      }
    }


/* ══════════════════════════════════════════════════════════════════════
   ks-billing-notice.v1.css
   ══════════════════════════════════════════════════════════════════════ */
/* The billing-restricted notice.
 *
 * Deliberately NOT styled as an error. A dojo whose card expired has not done
 * something wrong, and a red alarm across the top of their team space reads as
 * an accusation to every member who sees it. It is an amber, informational
 * band: unmissable, calm, and it names one action.
 *
 * The `data-billing-restricted` body attribute is set by the module and is
 * presentation only — it lets paid surfaces de-emphasise themselves without the
 * notice module knowing which they are. Every one of those surfaces is refused
 * server-side regardless, so this is communication and never enforcement.
 */
.ks-billing-notice {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: .6rem 1rem;
  padding: .85rem 1.25rem;
  background: #fdf1dc;
  border-bottom: 2px solid #d08706;
  color: #6b4708;
  font-family: var(--ks-body, inherit);
}

.ks-billing-notice[hidden] {
  display: none;
}

.ks-billing-notice-headline {
  margin: 0;
  font-family: var(--ks-display, inherit);
  font-size: 1rem;
  font-weight: 700;
  flex: 0 0 100%;
}

.ks-billing-notice-detail {
  margin: 0;
  font-size: .9rem;
  line-height: 1.5;
  flex: 1 1 20rem;
}

.ks-billing-notice-action {
  appearance: none;
  cursor: pointer;
  font: inherit;
  font-weight: 700;
  font-size: .85rem;
  padding: .5rem 1.1rem;
  border-radius: 999px;
  border: 1px solid #92620a;
  background: #92620a;
  color: #fff;
  white-space: nowrap;
}

.ks-billing-notice-action:hover:not(:disabled) {
  background: #6b4708;
  border-color: #6b4708;
}

.ks-billing-notice-action:disabled {
  opacity: .6;
  cursor: default;
}

.ks-billing-notice-action:focus-visible {
  outline: 2px solid #6b4708;
  outline-offset: 2px;
}

@media (max-width: 640px) {
  .ks-billing-notice {
    padding: .75rem 1rem;
  }

  .ks-billing-notice-action {
    flex: 1 1 100%;
    text-align: center;
  }
}


/* ══════════════════════════════════════════════════════════════════════
   dojo-tenant-surface-components.v1.css
   ══════════════════════════════════════════════════════════════════════ */
    /* ══════════════════════════════════════════════════════════════════════════
       TENANT SURFACE COMPONENTS — the shared pieces every tenant page is built
       from. Class-based and view-agnostic on purpose: the point of this file is
       that Team, Match Library, Events, Resources and Schedule inherit the same
       hero, the same buttons and the same tiles rather than each growing its own.

       Reads only --ks-* names, so every component answers for both themes with
       no duplicate rules. Nothing here is scoped to a view, and nothing here
       styles a legacy class, so it cannot change a page that has not adopted it.
       ══════════════════════════════════════════════════════════════════════════ */

    /* ══ ICONS ══════════════════════════════════════════════════════════════════
       The whole icon language, styled once. Sized in ems so an icon scales with
       the text it sits beside, and `currentColor` so it is that text's colour --
       on a card, on the band, in either theme, with nothing to configure.

       The sprite itself is a definitions block: hidden, out of flow, never
       content. See dojo-global-icon-system.v1.js.
       ═══════════════════════════════════════════════════════════════════════════ */
    .ks-icon {
      display: inline-block;
      width: 1.05em;
      height: 1.05em;
      /* The stroke contract lives here, not on the sprite. Presentation
         attributes set on the sprite's own <svg> do not reach a <use>
         instance in another SVG context, which is why the first pass rendered
         every glyph as a solid black blob: the paths were being filled with
         the default fill and never stroked. As CSS they inherit into the
         cloned content, and `currentColor` keeps an icon the colour of its
         text. */
      fill: none;
      stroke: currentColor;
      stroke-width: 1.75;
      stroke-linecap: round;
      stroke-linejoin: round;
      /* Optical centring against lower-case text; a baseline-aligned icon rides
         high next to a line of type. */
      vertical-align: -.19em;
      flex-shrink: 0;
      color: inherit;
    }

    #ks-icon-sprite,
    .ks-icon-sprite {
      position: absolute;
      width: 0;
      height: 0;
      overflow: hidden;
    }

    /* Metadata rows -- date, time, place -- give their icon the quiet ink so the
       value is what the eye lands on. */
    .cc-next-event-detail .ks-icon,
    .cc-event-card-meta-item .ks-icon,
    .home-calendar-meta .ks-icon,
    .ubc-meta .ks-icon {
      color: var(--ks-accent-strong);
      opacity: .85;
    }

    /* ══ BUTTONS ════════════════════════════════════════════════════════════════ */
    .ks-btn {
      display: inline-flex;
      align-items: center;
      justify-content: center;
      gap: .5rem;
      /* Bigger targets and bigger labels: the actions were .72rem, which is
         smaller than the metadata beside them. */
      padding: .95rem 1.75rem;
      border: 1px solid transparent;
      /* The DEFAULT control corner, not the dense one, and the padding above is
         the argument: .95rem/1.75rem is a larger button than .btn's 12/24, so
         taking the tighter radius had it reading as a smaller control than the
         one it sits beside. It was 4px (--radius-sm, the older family), which is
         how the demo dojo's home page came to render .btn at one corner and
         .ks-btn at another within the same viewport. Two button systems, one
         corner. */
      border-radius: var(--ks-r-control);
      font-family: 'Raleway', sans-serif;
      font-size: .8rem;
      font-weight: var(--ks-weight-action);
      letter-spacing: .1em;
      text-transform: uppercase;
      text-decoration: none;
      white-space: nowrap;
      cursor: pointer;
      transition:
        background var(--duration-fast) var(--ease-out),
        border-color var(--duration-fast) var(--ease-out),
        color var(--duration-fast) var(--ease-out);
    }

    .ks-btn-primary {
      background: var(--ks-accent);
      border-color: var(--ks-accent);
      color: var(--ks-on-accent);
    }

    .ks-btn-primary:hover {
      background: var(--ks-accent-strong);
      border-color: var(--ks-accent-strong);
      /* --ks-accent-strong is the dark gold in light and the pale gold in dark,
         so the label has to flip with it rather than stay one colour. */
      color: var(--ks-surface);
    }

    :root[data-theme="dark"] .ks-btn-primary:hover {
      color: var(--ks-on-accent);
    }

    .ks-btn-ghost {
      background: transparent;
      border-color: var(--ks-line);
      color: var(--ks-ink);
    }

    .ks-btn-ghost:hover {
      border-color: var(--ks-accent);
      background: var(--ks-accent-soft);
    }

    /* The ghost is an outline in whatever it sits on, and on the band that is
       not the content surface's ink. Scoped to .ks-band rather than given its
       own class, so a button placed in a band is right without the markup
       having to know where it is. */
    .ks-band .ks-btn-ghost {
      border-color: rgba(255, 255, 255, .30);
      color: var(--ks-band-ink);
    }

    .ks-band .ks-btn-ghost:hover {
      border-color: var(--ks-band-accent);
      background: var(--ks-band-accent-soft);
      color: var(--ks-band-ink);
    }

    .ks-band .ks-btn:focus-visible {
      outline-color: var(--ks-band-accent);
    }

    .ks-btn:focus-visible {
      outline: 2px solid var(--ks-focus-ring);
      outline-offset: 3px;
    }

    /* ══ CARD DEPTH ═════════════════════════════════════════════════════════════
       The product's one signature interaction: a card that can be opened lifts
       towards the reader and warms its edge to the brand gold.

       One rule for every openable card on every screen, rather than a hover
       treatment per grid. That is what makes it read as a property of the
       product instead of as a flourish somebody added to one page -- and it is
       why there is only one of these. A page where several things move for
       several reasons has no signature.

       Only cards that go somewhere. A card the reader cannot open must not
       behave as though they can.
       ═══════════════════════════════════════════════════════════════════════════ */
    .ks-media-card,
    .ks-roster-card {
      transition:
        transform var(--duration-base) var(--ease-out),
        box-shadow var(--duration-base) var(--ease-out),
        border-color var(--duration-base) var(--ease-out);
    }

    .ks-media-card:hover,
    .ks-roster-card:hover,
    .ks-media-card:focus-visible,
    .ks-roster-card:focus-visible {
      transform: translateY(-3px);
      box-shadow: var(--ks-shadow-3);
      border-color: var(--ks-accent);
    }

    /* The lift is decoration; the gold edge is the part that says "this opens".
       Somebody who has asked for less movement keeps the second and loses the
       first. */
    @media (prefers-reduced-motion: reduce) {

      .ks-media-card,
      .ks-roster-card {
        transition: border-color var(--duration-fast) var(--ease-out);
      }

      .ks-media-card:hover,
      .ks-roster-card:hover,
      .ks-media-card:focus-visible,
      .ks-roster-card:focus-visible {
        transform: none;
      }
    }

    /* ══ THE BAND ═══════════════════════════════════════════════════════════════
       One surface, two users: the home hero and the header of every other
       screen. Both are `.ks-band`, so a dojo's identity is the same object
       wherever you meet it and a new screen gets the treatment by naming the
       class rather than by copying forty lines.

       Dark in both themes on purpose. It is brand, like the nav above it and
       the footer below, and the three of them are what make a page read as a
       lit content surface held between two dark edges rather than as a form on
       a background.

       The Americana is deliberately almost subliminal: two soft washes at the
       far edge, well under the text, and a gold hairline along the foot. It
       should register as energy and heat, not as bunting. Nothing here is
       allowed onto a content surface.
       ═══════════════════════════════════════════════════════════════════════════ */
    .ks-band {
      position: relative;
      isolation: isolate;
      /* Layered, not one fill: an arena light from the top centre, the hue
         running deep-to-raised beneath it, and a floor vignette. One flat
         saturated wall of the tenant's colour read as a toy; the same hue lit
         like a venue reads as a place. All four layers are the tenant's own
         derived shades, so every dojo gets the depth in its own colour. */
      background:
        radial-gradient(110% 90% at 50% -12%, rgba(255, 255, 255, .14) 0%, transparent 55%),
        radial-gradient(130% 100% at 50% 118%, rgba(2, 5, 10, .55) 0%, transparent 58%),
        linear-gradient(155deg, var(--ks-band-deep) 0%, var(--ks-band) 52%, var(--ks-band-raised) 100%);
      color: var(--ks-band-ink);
      overflow: hidden;
    }



    /* The wash. Sized in percentages of the band so it stays in the same place
       on a phone as on a monitor, and kept to the top-right quadrant, which is
       the corner the copy never occupies. */
    .ks-band::before {
      content: '';
      position: absolute;
      inset: 0;
      z-index: -1;
      /* The hairline grain leads the stack: a whisper of tooth at three
         percent, so the lit surface does not read as smooth plastic. */
      background:
        repeating-linear-gradient(115deg, rgba(255, 255, 255, .025) 0 1px, transparent 1px 9px),
        radial-gradient(58% 88% at 92% 6%, var(--ks-band-red) 0%, transparent 62%),
        radial-gradient(70% 95% at 74% 0%, var(--ks-band-blue) 0%, transparent 64%),
        radial-gradient(46% 70% at 88% 88%, color-mix(in oklab, var(--accent) 13%, transparent) 0%, transparent 68%);
      pointer-events: none;
    }

    /* The one gold edge that is always on screen. Faded at both ends so it
       reads as a lit edge rather than a rule drawn across the page. */
    .ks-band::after {
      content: '';
      position: absolute;
      left: 0;
      right: 0;
      bottom: 0;
      height: 2px;
      background: linear-gradient(90deg,
          transparent 0%,
          var(--ks-band-edge) 18%,
          var(--ks-band-accent) 50%,
          var(--ks-band-edge) 82%,
          transparent 100%);
      pointer-events: none;
    }

    /* ══ THE FLAGSHIP STAGE ═════════════════════════════════════════════════
       The imagery half of a flagship band, as owned zones rather than layers.

         .ks-flag-inner
         ├─ .ks-flag-copy   kicker / title / lede / actions
         └─ .ks-flag-stage  grid, auto-flow column, mandatory gap
            ├─ .ks-flag-art    artwork or athlete cutout
            └─ .ks-flag-crest  the crest, its own cell

       Art and crest are grid siblings with a gap, so there is no viewport at
       which one can sit on the other -- the collision the absolutely
       positioned crest produced is impossible by construction, not avoided by
       tuning. A zone whose image is empty or failed collapses and its sibling
       takes the stage alone; with neither, the stage vanishes and the copy
       takes the measure. That is the whole configuration matrix: photo band,
       artwork, crest, any pair, any one, or nothing, from the same markup.
       ═══════════════════════════════════════════════════════════════════════ */
    .ks-flag-inner {
      display: flex;
      align-items: center;
      gap: clamp(1.5rem, 4vw, 3.5rem);
    }

    .ks-flag-copy {
      flex: 1 1 auto;
      min-width: 0;
    }

    /* The stage TAKES the width the copy does not, and centres its contents in
       it, rather than hugging the copy and leaving the remainder as a void.
       Sized to content, a narrow crest sat 64px from the text with ~230px of
       empty band after it -- copy, object, gap -- which reads as an image
       pushed against the column rather than a second column. Centring makes
       the space fall on both sides of the mark, so it is placed rather than
       left over, and it does so for any crest width without knowing one. */
    .ks-flag-stage {
      flex: 1 1 auto;
      display: grid;
      grid-auto-flow: column;
      justify-content: center;
      align-items: end;
      gap: clamp(1rem, 2.5vw, 2rem);
      min-width: 0;
    }

    .ks-flag-art {
      min-width: 0;
    }

    .ks-flag-art img {
      display: block;
      width: auto;
      max-width: 100%;
      max-height: clamp(240px, 32vw, 420px);
      object-fit: contain;
    }

    .ks-flag-crest {
      position: relative;
      align-self: center;
      flex-shrink: 0;
    }

    .ks-flag-crest img {
      display: block;
      width: auto;
      max-width: clamp(130px, 15vw, 210px);
      max-height: clamp(110px, 14vw, 180px);
      object-fit: contain;
      filter: drop-shadow(0 12px 28px rgba(0, 0, 0, .5));
    }

    /* A zone with nothing in it stops existing. */
    .ks-flag-art:has(> img[data-media-state="empty"]),
    .ks-flag-art:has(> img[data-media-state="error"]) {
      display: none;
    }

    /* Alone on the stage, the crest is the whole visual and stands at
       presentation size, with the drop shadow doing the seating. On the home
       hero -- the tallest band -- it goes larger still: crest-led is a chosen
       tenant mode, not a fallback, and it has to read as one. */
    .ks-flag-stage-crest-only .ks-flag-crest img,
    .ks-flag-stage:not(:has(.ks-flag-art img[data-media-state="present"])) .ks-flag-crest img {
      max-width: clamp(170px, 21vw, 300px);
      max-height: clamp(150px, 19vw, 260px);
    }

    /* ── The crest is sized by presence, not by extents ──────────────────────
       A max-width/max-height pair normalises how far a mark may reach, which is
       not what a reader sees. A wide ribboned crest fills the width and leaves
       vertical air; a tall shield fills the height and leaves horizontal air.
       Two tenants with equally good artwork therefore got visibly unequal
       marks: measured on the two real shapes at 1440, the shield came out at
       68% of the ribboned crest's area. Raising the caps narrowed that to 77%
       and could not close it, because a cap cannot know a silhouette.

       Holding AREA constant is what makes them equal, and area needs the ratio.
       ks-optical-crest.v1.js publishes it as --ks-crest-ratio, the same way this
       app already publishes data-media-state -- the runtime states a fact about
       the image, the stylesheet decides what it means.

       With area A and ratio r: h = sqrt(A/r), w = h*r. Expressed against a base
       height H where A = H^2, that is simply h = H / sqrt(r) -- one expression
       that gives every silhouette the same optical weight, with no per-shape
       tuning and nothing that knows a tenant.

       The caps stay as bounds rather than as the sizing mechanism: they stop a
       banner-shaped wordmark reaching across the band or a narrow crest towering
       over it. For ordinary crests they never bind.

       The gap to the copy is a flex gap on .ks-flag-inner and is untouched, so a
       crest grows into the empty band rather than toward the text. The
       two-column composition and the stage's no-collision guarantee both hold by
       construction. */
    /* Declared once, on the band, because two tiers need them and a media query
       cannot join a selector list. The crest-only tier below and the phone tier
       further down both read these, so the sizes have one home to change.

       --ks-hero-crest-max-h floors ABOVE the base height on purpose: a tall
       crest is sized TALLER than the base to reach the same area -- 190px of
       base becomes 232px for a shield -- so a floor at the base would clip
       exactly the silhouette this exists to rescue, and only on the smallest
       screens, where it is least forgiving. */
    .ks-hero {
      /* Trimmed 2026-08-15 (was 360/460/440): the crest is the hero's tallest
         column, so its ceiling IS the band's height. At 380px it still reads
         as the flagship mark while returning ~60px of first-viewport to the
         content below. The delivery images carry 3x of the 460px box, so no
         resolution is lost at any of these sizes. */
      --ks-hero-crest-base: clamp(180px, 22vw, 320px);
      --ks-hero-crest-max-w: clamp(230px, 30vw, 420px);
      --ks-hero-crest-max-h: clamp(230px, 27vw, 380px);
    }

    .ks-hero .ks-flag-stage:not(:has(.ks-flag-art img[data-media-state="present"])) .ks-flag-crest img {
      --ks-crest-h: var(--ks-hero-crest-base);
      max-width: var(--ks-hero-crest-max-w);
      max-height: var(--ks-hero-crest-max-h);
      filter: drop-shadow(0 18px 44px rgba(0, 0, 0, .55));
    }

    /* Applied only where a ratio has actually been published. A crest whose
       intrinsic size could not be read -- decorative, failed, not yet loaded --
       keeps the bounded behaviour above rather than being sized from a value
       nobody measured.

       The gate is data-ks-crest-sized, a state attribute the runtime sets beside
       the ratio, NOT [style*="--ks-crest-ratio"]. Matching on the text of a style
       attribute is the coupling this stylesheet spent a commit removing from the
       how-it-works view, and it would have been no better here. It also reads the
       same as data-media-state, which is the existing way this app says what an
       image slot currently is.

       One rule, not one per tier: --ks-crest-h is what varies, and a tier that
       does not set it leaves this invalid at computed-value time, so the caps
       above answer alone. */
    .ks-hero .ks-flag-crest img[data-ks-crest-sized] {
      height: calc(var(--ks-crest-h) / sqrt(var(--ks-crest-ratio)));
      width: auto;
    }

    /* When the band behind the stage is a photograph, the crest stands on a
       pedestal: a local darkening under its own zone, so it reads as placed on
       an authored panel rather than floating over whoever happens to be in
       that part of the picture. */
    .ks-band:has(.ks-band-photo-layer img[data-media-state="present"]) .ks-flag-crest::before {
      content: '';
      position: absolute;
      inset: -16% -14%;
      z-index: -1;
      background: radial-gradient(62% 62% at 50% 52%, rgba(4, 8, 16, .58) 0%, rgba(4, 8, 16, .30) 58%, transparent 78%);
      border-radius: 24px;
      pointer-events: none;
    }

    /* ── The flagship reveal ────────────────────────────────────────────────
       The one entrance in the product: copy, then stage, rising a line's
       height as they arrive. Backwards fill so nothing flashes unstyled, no
       loop, and none of it for a reader who asked for less motion. */
    @media (prefers-reduced-motion: no-preference) {

      .ks-flag .ks-flag-copy,
      .ks-flag .ks-flag-stage {
        animation: ksFlagReveal .55s var(--ease-out) backwards;
      }

      .ks-flag .ks-flag-stage {
        animation-delay: .12s;
      }
    }

    @keyframes ksFlagReveal {
      from {
        opacity: 0;
        translate: 0 14px;
      }

      to {
        opacity: 1;
        translate: 0 0;
      }
    }

    /* ── The flagship on a phone: a title page ──────────────────────────────
       Identity centres, crest above the title, and the stage gives up the
       artwork -- two stacked images push the page's actions below the fold and
       the crest is the identity. Only the band centres; everything below it is
       scanning material and stays left. */
    @media (max-width: 639px) {

      .ks-flag-inner {
        flex-direction: column-reverse;
        text-align: center;
        align-items: center;
      }

      /* The home hero is BOTH .ks-hero-inner and .ks-flag-inner, and the
         legacy hero-stacking rule further down (max-width: 959px, column /
         flex-start, written for the photo-hero era) ties this tier on
         specificity and beats it on source order — property by property. The
         result shipped: text-align: center survived unopposed while
         flex-direction and align-items lost, so the copy centred but the
         crest sat left-bottom — two design generations interleaved in one
         band. Measured on the live tenant. The doubled selector settles it
         in the flagship's favour at the width where the title-page
         composition is the design. 640–959 keeps the legacy left-stacked
         composition whole, which is coherent there. */
      .ks-hero-inner.ks-flag-inner {
        flex-direction: column-reverse;
        align-items: center;
      }

      .ks-flag-copy .ks-hero-actions,
      .ks-flag-copy .ks-pagehead-actions {
        justify-content: center;
      }

      .ks-flag-copy .ks-hero-lede,
      .ks-flag-copy .ks-pagehead-lede {
        margin-left: auto;
        margin-right: auto;
      }

      .ks-flag-stage .ks-flag-art {
        display: none;
      }

      .ks-flag-crest img,
      .ks-flag-stage:not(:has(.ks-flag-art img[data-media-state="present"])) .ks-flag-crest img {
        max-width: clamp(120px, 30vw, 150px);
        max-height: clamp(96px, 24vw, 130px);
      }

      /* The stage gave up its artwork three rules above, so on a phone the crest
         is alone on screen whatever the tenant configured. :has() reads the DOM
         rather than what is displayed, so the crest-only tier never matched
         here: a dojo owning BOTH a crest and artwork was handed the shared-stage
         size -- 64x96 for a shield -- justified by an image that is not on the
         page, while the same dojo without artwork got 155x233. Owning more
         assets made their identity smaller.

         This restates the alone-on-the-stage sizing for the hero without asking
         about artwork, which is the only honest question at this width. It sits
         below the crest-only tier's specificity on purpose: where that tier
         already applies it wins, and it reads the same three values.

         The height rule is not repeated here. It is one selector further up,
         gated on data-ks-crest-sized rather than on a tier, so setting the base
         is all a tier has to do. */
      .ks-hero .ks-flag-stage .ks-flag-crest img {
        --ks-crest-h: var(--ks-hero-crest-base);
        max-width: var(--ks-hero-crest-max-w);
        max-height: var(--ks-hero-crest-max-h);
      }

      /* On a phone the quick links are icon + title. The one-line ledes
         ellipsised at ~13 characters ("Practices & ev…") — half a sentence on
         every card is noise, and the titles carry the meaning whole. The
         ledes return at 640px where they fit. Subtraction, not compression. */
      .ks-quicklink-sub {
        display: none;
      }
    }

    /* ── The band, carrying a photograph ────────────────────────────────────
       A tenant-configured image under the wash: the layer caps its own opacity
       and a left scrim keeps the copy zone dark, so a bright photograph cannot
       take the title with it. A tenant with no image keeps the pure gradient --
       the layer collapses on the empty/error media states like every other
       tenant image slot. */
    .ks-band-photo-layer {
      position: absolute;
      inset: 0;
      z-index: -2;
      opacity: .32;
    }

    .ks-band-photo-layer img {
      display: block;
      width: 100%;
      height: 100%;
      object-fit: cover;
    }

    .ks-band-photo-layer img[data-media-state="empty"],
    .ks-band-photo-layer img[data-media-state="error"] {
      display: none;
    }

    /* The scrim, on the flagship variant only: the wash corner stays for
       energy, and the reading zone stays guaranteed. The two crisp diagonals
       sweeping the crest corner are the championship ribbon -- the same double
       stripe the crests themselves carry, drawn once in CSS rather than as an
       image, and kept behind the scrim's reach so the copy zone never fights
       them. */
    .ks-pagehead-flagship::before {
      background:
        repeating-linear-gradient(115deg, rgba(255, 255, 255, .025) 0 1px, transparent 1px 9px),
        linear-gradient(90deg, rgba(4, 8, 16, .82) 0%, rgba(4, 8, 16, .45) 46%, transparent 72%),
        linear-gradient(115deg,
          transparent 63%,
          rgba(178, 34, 52, .50) 63.4% 66.4%,
          rgba(255, 255, 255, .30) 66.8% 68.2%,
          rgba(178, 34, 52, .40) 68.6% 70.2%,
          transparent 70.6%),
        radial-gradient(58% 88% at 92% 6%, var(--ks-band-red) 0%, transparent 62%),
        radial-gradient(70% 95% at 74% 0%, var(--ks-band-blue) 0%, transparent 64%),
        radial-gradient(46% 70% at 88% 88%, color-mix(in oklab, var(--accent) 13%, transparent) 0%, transparent 68%);
    }

    /* (Mobile centering for flagship bands lives in the .ks-flag rules: the
       hub band carries both classes and one owner is enough.) */

    .ks-pagehead-actions {
      display: flex;
      flex-wrap: wrap;
      gap: .65rem;
      margin-top: clamp(1rem, 2.5vw, 1.5rem);
    }

    /* ══ ATHLETE BAND ═══════════════════════════════════════════════════════════
       The band, carrying a person instead of a page title: the competitor's
       breadcrumb, avatar, name, belt, season and the two participation figures
       the profile leads with.

       Its own block rather than a variant of .ks-pagehead because the content is
       a different shape -- an identity, not a heading -- but it is the same
       .ks-band underneath, so the profile opens the way every other screen does.
       ═══════════════════════════════════════════════════════════════════════════ */
    .ks-athlete-band {
      padding: calc(var(--ks-nav-h) + clamp(.75rem, 2vw, 1.25rem)) var(--ks-gutter) clamp(1.5rem, 3.5vw, 2.25rem);
    }

    .ks-athlete-band-inner {
      max-width: var(--ks-measure);
      margin: 0 auto;
    }

    .ks-athlete-band .ks-breadcrumb {
      background: none;
      border: 0;
      padding: 0;
      margin-bottom: clamp(.85rem, 2vw, 1.4rem);
    }

    .ks-athlete-band .ks-breadcrumb-inner {
      padding: 0;
      max-width: none;
    }

    .ks-athlete-band .ks-breadcrumb a {
      color: var(--ks-band-ink-3);
    }

    .ks-athlete-band .ks-breadcrumb a:hover {
      color: var(--ks-band-accent);
    }

    .ks-athlete-band .ks-breadcrumb-sep {
      color: var(--ks-band-ink-3);
    }

    .ks-athlete-band .ks-breadcrumb-current {
      color: var(--ks-band-ink);
    }

    /* Identity left, figures right, and they swap to stacked before either has
       to wrap inside itself. */
    .ks-athlete-identity {
      display: flex;
      align-items: center;
      gap: clamp(1rem, 3vw, 1.75rem);
      flex-wrap: wrap;
    }

    .ks-athlete-identity-copy {
      min-width: 0;
      /* Takes the slack, which pushes the figures to the far edge on a wide
         screen and lets them fall under the name on a narrow one. */
      flex: 1 1 14rem;
    }

    .ks-athlete-avatar {
      position: relative;
      width: clamp(72px, 9vw, 104px);
      height: clamp(72px, 9vw, 104px);
      flex-shrink: 0;
      border-radius: 50%;
      overflow: hidden;
      display: flex;
      align-items: center;
      justify-content: center;
      background: linear-gradient(160deg, var(--ks-band-raised), var(--ks-band-deep));
      border: 2px solid var(--ks-band-edge);
      box-shadow: 0 10px 30px rgba(0, 0, 0, .45);
    }

    .ks-athlete-initials {
      font-family: var(--ks-display);
      font-size: clamp(1.4rem, 2.6vw, 2rem);
      font-weight: 700;
      letter-spacing: .04em;
      color: var(--ks-band-accent);
    }

    .ks-athlete-name {
      font-family: var(--ks-display);
      font-size: clamp(1.7rem, 3.6vw, 2.6rem);
      font-weight: 700;
      line-height: 1.05;
      letter-spacing: -.005em;
      color: var(--ks-band-ink);
    }

    .ks-athlete-belt {
      margin-top: .3rem;
      font-size: clamp(.85rem, 1.3vw, .98rem);
      font-weight: 600;
      color: var(--ks-band-ink-2);
    }

    .ks-athlete-season {
      margin-top: .15rem;
      font-size: .72rem;
      font-weight: 700;
      letter-spacing: .14em;
      text-transform: uppercase;
      color: var(--ks-band-ink-3);
    }

    /* ══ HERO ═══════════════════════════════════════════════════════════════════
       Two columns that are not equal: the copy leads and the imagery supports it.
       The media column is `flex: 0 1 auto`, so a tenant that has uploaded neither
       a photograph nor a crest gets a full-width statement rather than a page
       with a hole in it.
       ═══════════════════════════════════════════════════════════════════════════ */
    .ks-hero {
      position: relative;
      color: var(--ks-band-ink);
      /* The nav allowance plus BREATHING, not a canvas: the band's height
         should come from its content. Measured 2026-08-15 before the trim,
         the homann hero held 192px above its headline against 64px below its
         actions — with the view shell's duplicate allowance removed (see
         dojo-global-document-view-shell.v1.css), this clamp is the whole of
         the space above the identity. */
      padding: calc(var(--ks-nav-h) + clamp(1.25rem, 2.5vw, 2rem)) var(--ks-gutter) clamp(1.75rem, 4vw, 3rem);
      overflow: hidden;
    }

    /* The hero's own light source sits on top of the band's wash rather than
       replacing it -- .ks-band::before is the wash, this is the key light on
       the subject. Same in both themes now that the band is. */
    .ks-hero::before {
      content: '';
      position: absolute;
      inset: 0;
      z-index: -1;
      background: radial-gradient(96% 78% at 76% 18%, rgba(255, 255, 255, .09) 0%, transparent 64%);
      pointer-events: none;
    }

    .ks-hero-inner {
      position: relative;
      max-width: var(--ks-measure);
      margin: 0 auto;
      display: flex;
      align-items: center;
      gap: clamp(1.5rem, 5vw, 4rem);
    }

    .ks-hero-copy {
      flex: 1 1 auto;
      min-width: 0;
      /* Wide enough that a three-word dojo name lands on two lines at the
         display step rather than three. */
      max-width: 40rem;
    }

    .ks-hero-title {
      font-family: var(--ks-display);
      /* The display step: a flagship title should be the largest thing on the
         page by a clear margin, not a slightly bigger heading. */
      font-size: var(--ks-display-1);
      font-weight: var(--ks-weight-display);
      line-height: 1.04;
      letter-spacing: -.01em;
      color: var(--ks-band-ink);
      margin: 0 0 clamp(.75rem, 2vw, 1.1rem);
      text-wrap: balance;
    }

    .ks-hero-lede {
      font-size: var(--ks-body-lg);
      line-height: 1.6;
      font-weight: 450;
      color: var(--ks-band-ink-2);
      /* Measured in characters, so the line length stays readable at any size. */
      max-width: 44ch;
      margin: 0 0 clamp(1.4rem, 3vw, 2rem);
    }

    .ks-hero-actions {
      display: flex;
      flex-wrap: wrap;
      gap: .75rem;
      margin-bottom: clamp(1.5rem, 4vw, 2.5rem);
    }

    /* The platform credit: present on every tenant page, and quiet enough that
       the dojo's own name is what a visitor reads first. */
    .ks-hero-powered {
      font-size: .68rem;
      letter-spacing: .1em;
      text-transform: uppercase;
      color: var(--ks-band-ink-3);
      margin: 0;
    }

    .ks-hero-powered a {
      color: var(--ks-band-accent);
      font-weight: 700;
      text-decoration: none;
    }

    .ks-hero-powered a:hover {
      text-decoration: underline;
    }

    .ks-hero-media {
      position: relative;
      flex: 0 1 auto;
      display: flex;
      align-items: flex-end;
      justify-content: center;
      min-width: 0;
    }

    .ks-hero-photo {
      display: block;
      width: auto;
      max-width: 100%;
      max-height: clamp(260px, 42vw, 460px);
      object-fit: contain;
    }

    /* Both slots collapse when the tenant has not filled them, so the layout
       closes up instead of holding a gap for an image that is not coming. */
    .ks-hero-photo[data-media-state="empty"],
    .ks-hero-photo[data-media-state="error"],
    .ks-hero-crest-img[data-media-state="empty"],
    .ks-hero-crest-img[data-media-state="error"] {
      display: none;
    }

    /* ...and "not coming" is not the same as "not here yet", which is the
       distinction the rule above could not make on its own.

       The crest lands in two stages — the settings document names it, then the
       image downloads — and until the pixels arrive the img has no intrinsic
       size, so the band collapses to its text height. Measured on production
       at 390px: 336px for the first 2-5 seconds of a cold load, then 557px.
       On a phone that is the whole difference between a dojo's front door and
       a list of links, because .ks-flag-art is hidden below 640px and the
       crest is the band's only image.

       So the space is reserved only while the crest is genuinely in flight.
       dojo-homann-site-media-admin.v2.js owns the class: it goes on when the
       media source starts and comes off on the IMAGE's load or error, on a
       resolution that names no crest, or after an 8s stall — so a dojo that
       has never uploaded one still gets the collapse above, within a tick.

       The reservation is --ks-hero-crest-max-h, the cap the crest is sized
       against, so the held box is the box that is coming rather than a number
       invented here: 230px at phone widths against a measured 220px, 345px at
       1280px against a measured 345px. */
    html.ks-home-crest-pending .ks-hero .ks-flag-crest {
      min-height: var(--ks-hero-crest-max-h, clamp(230px, 27vw, 380px));
    }

    @media (prefers-reduced-motion: no-preference) {
      .ks-hero .ks-flag-crest {
        transition: min-height .2s var(--ks-ease-out, ease-out);
      }
    }

    .ks-hero-crest {
      position: relative;
      flex-shrink: 0;
      align-self: center;
    }

    /* The crest no longer positions into the photograph's corner -- that rule
       was the collision Jason rejected: layering, not composition. Art and
       crest are grid siblings in .ks-flag-stage now, and the stage owns the
       relationship. */
    .ks-hero-crest-img {
      display: block;
      width: auto;
      max-width: 100%;
      max-height: clamp(110px, 15vw, 190px);
      object-fit: contain;
      filter: drop-shadow(0 12px 28px rgba(0, 0, 0, .5));
    }

    /* ══ QUICK LINKS ════════════════════════════════════════════════════════════
       One row of equal destinations. It sits on the canvas directly under the
       hero and reads as the page's first action, which is why it is a single
       panel with dividers rather than six separate cards.
       ═══════════════════════════════════════════════════════════════════════════ */
    .ks-quicklinks {
      background: var(--ks-canvas);
      padding: 0 var(--ks-gutter);
      /* Pulled up so the panel straddles the hero edge and ties the two together. */
      margin-top: clamp(-2.75rem, -4vw, -1.75rem);
      position: relative;
      z-index: 1;
    }

    .ks-quicklinks-inner {
      max-width: var(--ks-measure);
      margin: 0 auto;
      display: grid;
      grid-template-columns: repeat(2, minmax(0, 1fr));
      background: var(--ks-surface);
      border: 1px solid var(--ks-line);
      border-radius: var(--radius-lg);
      box-shadow: var(--ks-shadow-2);
      overflow: hidden;
    }

    .ks-quicklink {
      display: flex;
      align-items: center;
      gap: .7rem;
      padding: clamp(.85rem, 2vw, 1.15rem) clamp(.85rem, 2vw, 1.1rem);
      text-decoration: none;
      color: var(--ks-ink);
      cursor: pointer;
      /* Hairlines drawn with a shadow rather than a border so no tile in the
         grid needs to know whether it is on an edge. */
      box-shadow: 1px 0 0 var(--ks-line-soft), 0 1px 0 var(--ks-line-soft);
      transition: background var(--duration-fast) var(--ease-out);
    }

    .ks-quicklink:hover {
      background: var(--ks-accent-soft);
    }

    .ks-quicklink:focus-visible {
      outline: 2px solid var(--ks-focus-ring);
      outline-offset: -2px;
    }

    .ks-quicklink-icon {
      display: inline-flex;
      align-items: center;
      justify-content: center;
      width: 40px;
      height: 40px;
      flex-shrink: 0;
      font-size: 1.05rem;
      line-height: 1;
      /* The tenant's accent, quietly: a tinted chip with the accent's ink,
         not a grey box. On hover the chip commits to the accent fully — one
         clear state change instead of decoration. */
      background: var(--ks-accent-soft);
      color: var(--ks-accent-strong);
      border-radius: var(--radius-md);
      transition:
        background var(--duration-fast) var(--ease-out),
        color var(--duration-fast) var(--ease-out);
    }

    .ks-quicklink:hover .ks-quicklink-icon {
      background: var(--ks-accent);
      color: var(--ks-on-accent);
    }

    .ks-quicklink-text {
      display: flex;
      flex-direction: column;
      gap: .1rem;
      min-width: 0;
    }

    .ks-quicklink-title {
      font-size: .95rem;
      font-weight: 700;
      letter-spacing: .01em;
      color: var(--ks-ink);
    }

    .ks-quicklink-sub {
      font-size: .8rem;
      color: var(--ks-ink-3);
      overflow: hidden;
      text-overflow: ellipsis;
      white-space: nowrap;
    }

    /* ══ RESPONSIVE ═════════════════════════════════════════════════════════════
       Mobile is composed, not compressed. Below md the hero stacks with the
       imagery beneath the words it supports, and the quick links stay a two-up
       grid rather than becoming six full-width rows nobody scrolls past.
       ═══════════════════════════════════════════════════════════════════════════ */
    @media (max-width: 1100px) {

      /* The site header is 60px below this width. */
      .ks-hero,
      .ks-pagehead {
        --ks-nav-h: 60px;
      }
    }

    @media (max-width: 959px) {
      .ks-hero-inner {
        flex-direction: column;
        align-items: flex-start;
        gap: clamp(1.25rem, 4vw, 2rem);
      }

      .ks-hero-copy {
        max-width: none;
      }

      .ks-hero-media {
        width: 100%;
        justify-content: flex-start;
      }

      .ks-hero-photo {
        max-height: clamp(200px, 46vw, 300px);
      }
    }

    @media (min-width: 640px) {
      .ks-quicklinks-inner {
        grid-template-columns: repeat(3, minmax(0, 1fr));
      }
    }

    @media (min-width: 960px) {
      /* One track per link that is actually shown, rather than six regardless.
         The row is not a fixed set: Match Library is team-only, so a visitor and
         a non-team member see five entrances and a hard-coded six left a visible
         empty cell at the end of the row -- an absence where the reader has been
         taught to expect a card. Auto-flow counts the children instead, so the
         row stays whole however many entrances a reader has earned. */
      .ks-quicklinks-inner {
        grid-template-columns: none;
        grid-auto-flow: column;
        grid-auto-columns: minmax(0, 1fr);
      }

      .ks-quicklink {
        flex-direction: column;
        align-items: flex-start;
        gap: .55rem;
        padding: 1.25rem 1.1rem;
      }
    }

    @media (prefers-reduced-motion: reduce) {

      .ks-btn,
      .ks-quicklink {
        transition: none;
      }
    }

    /* ══ HIGHLIGHT BANNER ═══════════════════════════════════════════════════════
       A photograph and the announcement it belongs to, in one band. The body is
       #cc-announcement-section, which renderAnnouncementBar hides when there is
       nothing to say -- so the photograph takes the full width on its own rather
       than sitting beside an empty column.
       ═══════════════════════════════════════════════════════════════════════════ */
    .ks-highlight {
      background: var(--ks-canvas);
      padding: 0 var(--ks-gutter) var(--ks-section-gap);
    }

    .ks-highlight-inner {
      max-width: var(--ks-measure);
      margin: 0 auto;
      display: flex;
      flex-wrap: wrap;
      align-items: stretch;
      background: var(--ks-surface);
      border: 1px solid var(--ks-line);
      border-radius: var(--radius-lg);
      box-shadow: var(--ks-shadow-1);
      overflow: hidden;
    }

    .ks-highlight-media {
      position: relative;
      /* The photograph supports the announcement rather than competing with it,
         so it takes the smaller share and stops growing. */
      flex: 1 1 20rem;
      max-width: 42%;
      min-width: 0;
      min-height: 14rem;
    }

    /* Unless there is no announcement, which is the ordinary state of a dojo
       that has not written one. The cap was unconditional, so the photograph
       stayed at 42% and the other 58% was blank card -- the largest empty
       object on the page, on the tenants most likely to be looked at first. */
    .ks-highlight-inner:has(> .ks-highlight-body[hidden]) .ks-highlight-media {
      max-width: 100%;
    }

    .ks-highlight-photo {
      display: block;
      width: 100%;
      height: 100%;
      object-fit: cover;
      /* A photograph supports the section; it must not BE the page. Without a
         ceiling, a dojo with no announcement handed the full content measure
         to the image — measured on homann 2026-08-15: 1310x983 CSS px of team
         photo, most of a viewport. The ceiling scales with the viewport and
         the crop keeps faces: team photographs stack their subjects in the
         upper half, so the position bias trims floor and foreground first. */
      max-height: clamp(340px, 42vw, 520px);
      object-position: 50% 22%;
    }

    /* A dojo that has not uploaded a team photograph shows no photograph.
       This slot used to fall back to a shipped picture of Team USA, so every
       dojo without its own image presented a national team as its team — a
       claim none of them can make.

       The photograph goes; the ZONE does not. Hiding the whole
       .ks-highlight-media took the coach's upload control with it, because
       #btn-upload-team is a child of that box — so a dojo with no team photo
       had no way to add one, which is every dojo on its first day. The zone
       stops claiming a column instead: with the image gone it sizes to what is
       left, which is nothing for a reader and the upload control for a coach. */
    .ks-highlight-photo[data-media-state="empty"],
    .ks-highlight-photo[data-media-state="error"] {
      display: none;
    }

    .ks-highlight-media:has(> img[data-media-state="empty"]),
    .ks-highlight-media:has(> img[data-media-state="error"]) {
      flex: 0 0 auto;
      max-width: none;
      /* `min-height: 14rem` above is the height of a PHOTOGRAPH, and it applied
         whether or not there was one. The rule said the zone sizes to what is
         left; the floor meant it never did, so an empty slot still claimed
         224px. Measured on preview-dojo 2026-08-14: a bordered white card
         1120x226 with nothing inside it, under a "Team Updates" heading.
         Zero here is what makes the sentence above true. */
      min-height: 0;
    }

    /* No photograph AND no announcement: there is nothing left to frame, so the
       card chrome goes too. Without this the fix above trades a 226px empty box
       for a hairline empty one.

       The ZONE still stays — #btn-upload-team is inside it, and this is exactly
       the state where an admin needs it (see the note above about hiding chrome
       never hiding a control). A reader sees nothing; a coach sees the control,
       and the box sizes to it. */
    .ks-highlight-inner:has(> .ks-highlight-media > img[data-media-state="empty"]):has(> .ks-highlight-body[hidden]),
    .ks-highlight-inner:has(> .ks-highlight-media > img[data-media-state="error"]):has(> .ks-highlight-body[hidden]) {
      background: none;
      border-color: transparent;
      box-shadow: none;
    }

    .ks-highlight-inner:has(> .ks-highlight-media > img[data-media-state="empty"]) .ks-highlight-body,
    .ks-highlight-inner:has(> .ks-highlight-media > img[data-media-state="error"]) .ks-highlight-body {
      flex-basis: 100%;
    }

    .ks-highlight-body {
      flex: 1 1 20rem;
      min-width: 0;
      display: flex;
      flex-direction: column;
      justify-content: center;
      gap: .7rem;
      padding: clamp(1.5rem, 4vw, 2.75rem);
    }

    /* The announcement's own bar loses its card treatment in here: the banner is
       already the surface, and a card inside a card reads as a mistake. */
    .ks-highlight-body .cc-announcement {
      display: flex;
      flex-direction: column;
      align-items: flex-start;
      gap: .9rem;
      text-align: left;
    }

    /* ...unless the dojo asked for an occasion look, which IS a banner.
       Measured 2026-08-22 on the live shell: with `smart-announcement
       smart-announcement-holiday-premium smart-intensity-refined` all present
       on the bar, background-image computed to `none` and box-shadow to
       `none`. This block was why. It matches at (0,2,0) — the same weight as
       every rule in dojo-smart-announcement-premium-styles.v1.css — and loads
       130 files later in the bundle, so it won on order and flattened the
       whole feature down to a text colour. Eight occasion looks and three
       intensities rendered identically, and an admin toggling between them saw
       nothing change. Excluding the one case the reset was never meant to
       cover is the fix; the reset itself was right for the ordinary bar. */
    .ks-highlight-body .cc-announcement:not(.smart-announcement) {
      background: none;
      border: 0;
      border-radius: 0;
      padding: 0;
      box-shadow: none;
    }

    .ks-highlight-body .cc-announcement-text {
      /* The bar centres its own text; in here the block has a reading edge. */
      text-align: left;
      font-family: var(--ks-display);
      font-size: clamp(1.25rem, 2.6vw, 1.85rem);
      font-weight: 700;
      line-height: 1.2;
      letter-spacing: -.005em;
      color: var(--ks-ink);
      /* The shimmer is a gold gradient clipped to the text, which on a white
         surface leaves it barely legible. */
      background: none;
      -webkit-background-clip: initial;
      background-clip: initial;
      -webkit-text-fill-color: currentColor;
    }

    .ks-highlight-body .cc-announcement-link {
      display: inline-flex;
      align-items: center;
      gap: .4rem;
      padding: .8rem 1.5rem;
      border: 1px solid var(--ks-line);
      border-radius: var(--radius-sm);
      font-size: .7rem;
      font-weight: 800;
      letter-spacing: .14em;
      text-transform: uppercase;
      color: var(--ks-ink);
      text-decoration: none;
      white-space: nowrap;
      transition:
        background var(--duration-fast) var(--ease-out),
        border-color var(--duration-fast) var(--ease-out);
    }

    .ks-highlight-body .cc-announcement-link:hover {
      background: var(--ks-accent-soft);
      border-color: var(--ks-accent);
      text-decoration: none;
    }

    @media (max-width: 767px) {
      .ks-highlight-media {
        flex-basis: 100%;
        max-width: none;
        min-height: 12rem;
      }
    }

    /* ══ PAGE HEAD ══════════════════════════════════════════════════════════════
       The hero's smaller sibling, for every page that is not Home. Same warm
       band and same type, without the photograph: there is one tenant hero image
       slot and it belongs to the front page, so an inner page states where it is
       and gets out of the way.
       ═══════════════════════════════════════════════════════════════════════════ */
    .ks-pagehead {
      position: relative;
      color: var(--ks-band-ink);
      padding: calc(var(--ks-nav-h) + clamp(1.25rem, 3vw, 2.25rem)) var(--ks-gutter) clamp(1.75rem, 4vw, 2.75rem);
      overflow: hidden;
    }

    /* Layered over the band's wash, like the hero's. */
    .ks-pagehead::before {
      content: '';
      position: absolute;
      inset: 0;
      z-index: -1;
      background: radial-gradient(105% 88% at 80% 10%, rgba(255, 255, 255, .07) 0%, transparent 62%);
      pointer-events: none;
    }

    .ks-pagehead-inner {
      position: relative;
      max-width: var(--ks-measure);
      margin: 0 auto;
      display: flex;
      align-items: center;
      justify-content: space-between;
      gap: clamp(1.25rem, 4vw, 3rem);
    }

    .ks-pagehead-copy {
      min-width: 0;
    }

    .ks-pagehead-kicker {
      display: block;
      font-size: .66rem;
      font-weight: 800;
      letter-spacing: .18em;
      text-transform: uppercase;
      color: var(--ks-band-accent);
      margin-bottom: .5rem;
    }

    .ks-pagehead-title {
      font-family: var(--ks-display);
      font-size: var(--ks-display-2);
      font-weight: var(--ks-weight-display);
      line-height: 1.06;
      letter-spacing: -.01em;
      color: var(--ks-band-ink);
      margin: 0;
    }

    .ks-pagehead-lede {
      font-size: var(--ks-body-lg);
      line-height: 1.55;
      color: var(--ks-band-ink-2);
      max-width: 48ch;
      margin: .6rem 0 0;
    }

    .ks-pagehead-crest {
      /* Anchors the slot's staff edit control (.admin-upload-btn), which is
         absolutely positioned against the image it edits. */
      position: relative;
      flex-shrink: 0;
    }

    .ks-pagehead-crest img {
      display: block;
      width: auto;
      max-height: clamp(64px, 9vw, 112px);
      object-fit: contain;
      /* Lifted off the band. Most dojo crests are dark metal and gold and would
         otherwise sink into it. */
      filter: drop-shadow(0 8px 22px rgba(0, 0, 0, .55));
    }

    /* ── Optical tier for the page-head crest ──────────────────────────────
       Same law as the hero flag crest: hold AREA constant, h = H / sqrt(ratio),
       published by ks-optical-crest.v1.js as --ks-crest-ratio. The box caps
       above keep answering for crests whose ratio never arrives. The base H is
       deliberately larger than the old box cap -- measured against the real
       tenants, the box was underselling every mark with internal margins --
       and the max bounds stop a banner wordmark or a narrow totem from
       breaking the band composition. */
    .ks-pagehead-crest img[data-ks-crest-sized] {
      height: calc(clamp(84px, 11vw, 156px) / sqrt(var(--ks-crest-ratio)));
      width: auto;
      max-height: 200px;
      max-width: clamp(180px, 30vw, 340px);
    }

    .ks-pagehead-crest img[data-media-state="empty"],
    .ks-pagehead-crest img[data-media-state="error"] {
      display: none;
    }

    @media (max-width: 767px) {
      .ks-pagehead-crest {
        display: none;
      }
    }

    /* ── The staff media-edit control ────────────────────────────────────────
       One pencil per editable image slot (crest, header logo, team photo, the
       three page heroes). Role code reveals it with display:flex; signed-out
       and non-staff readers never see it. It sits ON the image it edits, in a
       corner, so an empty slot still shows the control where the image will
       land. The nav-logo instance narrows itself with inline size overrides —
       the header has no room for the full target. */
    .admin-upload-btn {
      position: absolute;
      right: .45rem;
      bottom: .45rem;
      z-index: 5;
      align-items: center;
      justify-content: center;
      width: 34px;
      height: 34px;
      padding: 0;
      border: 1px solid var(--ks-line);
      border-radius: var(--radius-full);
      background: var(--ks-surface);
      color: var(--ks-ink-2);
      font-size: .85rem;
      line-height: 1;
      cursor: pointer;
      box-shadow: var(--ks-shadow-1);
      transition:
        box-shadow var(--duration-fast) var(--ease-out),
        border-color var(--duration-fast) var(--ease-out),
        color var(--duration-fast) var(--ease-out);
    }

    .admin-upload-btn:hover {
      color: var(--ks-accent);
      border-color: var(--ks-accent);
      box-shadow: var(--ks-shadow-2);
    }

    .admin-upload-btn:focus-visible {
      outline: 2px solid var(--ks-accent);
      outline-offset: 2px;
    }

    /* ══ SECTION ════════════════════════════════════════════════════════════════ */
    .ks-section {
      background: var(--ks-canvas);
      padding: clamp(1.75rem, 4vw, 2.75rem) var(--ks-gutter) var(--ks-section-gap);
    }

    .ks-section-inner {
      max-width: var(--ks-measure);
      margin: 0 auto;
    }

    .ks-section-head {
      display: flex;
      flex-wrap: wrap;
      align-items: center;
      justify-content: space-between;
      gap: .75rem 1.5rem;
      margin-bottom: clamp(1rem, 2.5vw, 1.5rem);
    }

    .ks-section-title {
      font-family: var(--ks-display);
      font-size: clamp(1rem, 2vw, 1.2rem);
      font-weight: 700;
      letter-spacing: .12em;
      text-transform: uppercase;
      color: var(--ks-ink);
      margin: 0;
    }

    /* A one-line description under a section title, wrapping to its own row
       inside the flex head. */
    .ks-section-lede {
      flex-basis: 100%;
      margin: -.4rem 0 0;
      font-size: .85rem;
      color: var(--ks-ink-2);
    }

    .ks-search {
      display: flex;
      align-items: center;
      gap: .5rem;
      flex: 0 1 22rem;
      min-width: 0;
      background: var(--ks-surface);
      border: 1px solid var(--ks-line);
      border-radius: var(--radius-full);
      padding: .5rem .95rem;
    }

    .ks-search:focus-within {
      border-color: var(--ks-accent);
      box-shadow: 0 0 0 3px var(--ks-accent-soft);
    }

    .ks-search input {
      flex: 1;
      min-width: 0;
      border: 0;
      background: none;
      color: var(--ks-ink);
      font-size: .85rem;
      outline: none;
    }

    .ks-search input::placeholder {
      color: var(--ks-ink-3);
    }

    /* ══ ROSTER ═════════════════════════════════════════════════════════════════
       Portrait-led tiles: the photograph is what a parent scanning for their own
       child recognises first, so it leads and the text supports it.
       ═══════════════════════════════════════════════════════════════════════════ */
    .ks-roster {
      display: grid;
      grid-template-columns: repeat(auto-fill, minmax(9.5rem, 1fr));
      gap: clamp(.75rem, 2vw, 1.25rem);
    }

    .ks-roster-card {
      display: flex;
      flex-direction: column;
      text-align: left;
      padding: 0;
      background: var(--ks-surface);
      border: 1px solid var(--ks-line);
      border-radius: var(--radius-lg);
      box-shadow: var(--ks-shadow-1);
      overflow: hidden;
      cursor: pointer;
      font: inherit;
      color: inherit;
      transition:
        box-shadow var(--duration-base) var(--ease-out),
        border-color var(--duration-base) var(--ease-out);
    }

    .ks-roster-card:hover {
      box-shadow: var(--ks-shadow-2);
      border-color: var(--ks-accent);
    }

    .ks-roster-card:focus-visible {
      outline: 2px solid var(--ks-focus-ring);
      outline-offset: 2px;
    }

    .ks-roster-media {
      position: relative;
      display: block;
      aspect-ratio: 1;
      background: var(--ks-surface-sunken);
      overflow: hidden;
    }

    .ks-roster-photo {
      position: absolute;
      inset: 0;
      width: 100%;
      height: 100%;
      object-fit: cover;
    }

    /* The shared avatar photograph, everywhere initials used to stand alone:
       the Team Hub identity mark, the Tournament Hub member card and the
       admin roster tables all layer this over their monogram, so a photo that
       fails to load (or never existed) reveals the initials beneath — the
       same contract the roster card established. The parents are made
       positioning contexts here rather than in three separate sheets, so the
       overlay cannot silently detach from its circle. */
    .ksh-id-mark,
    .ks-hub-avatar,
    .adm-avatar {
      position: relative;
      overflow: hidden;
    }

    .ks-avatar-photo {
      position: absolute;
      inset: 0;
      width: 100%;
      height: 100%;
      object-fit: cover;
      border-radius: inherit;
    }

    /* Sits under the photograph, so a portrait that fails to load reveals it
       rather than leaving an empty square. */
    .ks-roster-initials {
      position: absolute;
      inset: 0;
      display: flex;
      align-items: center;
      justify-content: center;
      font-family: var(--ks-display);
      font-size: 1.5rem;
      font-weight: 700;
      color: var(--ks-ink-3);
    }

    /* The belt is the one piece of rank the roster carries, so it is a band on
       the portrait rather than another line of text. */
    .ks-roster-belt {
      position: absolute;
      left: 0;
      right: 0;
      bottom: 0;
      height: 5px;
      z-index: 2;
    }

    .ks-roster-body {
      display: flex;
      flex-direction: column;
      gap: .12rem;
      padding: .7rem .8rem .85rem;
      min-width: 0;
    }

    .ks-roster-name {
      font-size: .85rem;
      font-weight: 700;
      color: var(--ks-ink);
      overflow: hidden;
      text-overflow: ellipsis;
      white-space: nowrap;
    }

    .ks-roster-belt-label {
      font-size: .68rem;
      color: var(--ks-ink-2);
      overflow: hidden;
      text-overflow: ellipsis;
      white-space: nowrap;
    }

    .ks-roster-meta {
      font-size: .66rem;
      color: var(--ks-ink-3);
    }

    .ks-empty {
      grid-column: 1 / -1;
      padding: 2.5rem 1rem;
      text-align: center;
      color: var(--ks-ink-3);
      font-size: .85rem;
    }

    @media (prefers-reduced-motion: reduce) {
      .ks-roster-card {
        transition: none;
      }
    }

    /* Visible to a screen reader, not on screen: for a control whose purpose is
       obvious by sight but has no text a reader can announce. */
    .sr-only {
      position: absolute;
      width: 1px;
      height: 1px;
      margin: -1px;
      padding: 0;
      overflow: hidden;
      clip: rect(0, 0, 0, 0);
      white-space: nowrap;
      border: 0;
    }

    .ks-pagehead-crest {
      position: relative;
    }

    /* ══ TABS ═══════════════════════════════════════════════════════════════════
       Paired with dojo-ks-tabs.v1.js, which applies the roles, ids and roving
       tabindex. A surface declares the tabs; nothing here assumes which.
       ═══════════════════════════════════════════════════════════════════════════ */
    .ks-tablist {
      display: flex;
      gap: .25rem;
      border-bottom: 1px solid var(--ks-line);
      margin-bottom: clamp(1.25rem, 3vw, 1.75rem);
      overflow-x: auto;
      scrollbar-width: none;
    }

    .ks-tablist::-webkit-scrollbar {
      display: none;
    }

    /* On a phone the row wraps instead of scrolling. It scrolled, with the
       scrollbar hidden above -- so on a 390px screen the competitor profile's
       last two tabs sat past the right edge with nothing to say they were
       there, and Settings, which is where a competitor hides their own profile,
       was simply unreachable.

       Wrapping rather than adding a fade or arrows: five short labels over two
       rows is legible, needs no affordance to explain it, and every tab is one
       tap away. */
    @media (max-width: 639px) {
      .ks-tablist {
        flex-wrap: wrap;
        overflow-x: visible;
        gap: .15rem .35rem;
      }
    }

    .ks-tab {
      flex-shrink: 0;
      appearance: none;
      background: none;
      border: 0;
      /* The underline is drawn on the element itself so the tab does not shift
         by a pixel as it becomes current. */
      border-bottom: 2px solid transparent;
      margin-bottom: -1px;
      padding: .7rem 1rem;
      font-family: 'Raleway', sans-serif;
      font-size: .74rem;
      font-weight: 800;
      letter-spacing: .12em;
      text-transform: uppercase;
      color: var(--ks-ink-3);
      cursor: pointer;
      transition: color var(--duration-fast) var(--ease-out);
    }

    .ks-tab:hover {
      color: var(--ks-ink);
    }

    .ks-tab.is-active {
      color: var(--ks-ink);
      border-bottom-color: var(--ks-accent);
    }

    .ks-tab:focus-visible {
      outline: 2px solid var(--ks-focus-ring);
      outline-offset: -2px;
      border-radius: var(--radius-sm);
    }

    .ks-tabpanel:focus-visible {
      outline: 2px solid var(--ks-focus-ring);
      outline-offset: 4px;
      border-radius: var(--radius-md);
    }

    @media (prefers-reduced-motion: reduce) {
      .ks-tab {
        transition: none;
      }
    }

    /* ══ BREADCRUMB ═════════════════════════════════════════════════════════════
       A line telling the reader where they are and how to step back, sized like
       one. Distinct from the full-width route bands, which are page headers.
       ═══════════════════════════════════════════════════════════════════════════ */
    .ks-breadcrumb {
      background: var(--ks-canvas);
      padding: calc(var(--ks-nav-h) + .9rem) var(--ks-gutter) 0;
    }

    .ks-breadcrumb-inner {
      max-width: var(--ks-measure);
      margin: 0 auto;
      display: flex;
      align-items: center;
      gap: .5rem;
      font-size: .72rem;
      letter-spacing: .04em;
      color: var(--ks-ink-3);
    }

    .ks-breadcrumb a {
      color: var(--ks-ink-2);
      text-decoration: none;
      cursor: pointer;
      border-radius: var(--radius-sm);
    }

    .ks-breadcrumb a:hover {
      color: var(--ks-ink);
      text-decoration: underline;
    }

    .ks-breadcrumb a:focus-visible {
      outline: 2px solid var(--ks-focus-ring);
      outline-offset: 2px;
    }

    .ks-breadcrumb-current {
      color: var(--ks-ink);
      font-weight: 700;
    }

    @media (max-width: 1100px) {
      .ks-breadcrumb {
        --ks-nav-h: 60px;
      }
    }

    /* ══ MEDIA CARD ═════════════════════════════════════════════════════════════
       An image-led card for a thing you open: a tournament, a collection. The
       artwork carries the recognition and the text underneath carries the facts,
       so the two never fight for the same space.
       ═══════════════════════════════════════════════════════════════════════════ */
    .ks-cardgrid {
      display: grid;
      grid-template-columns: repeat(auto-fill, minmax(15rem, 1fr));
      gap: clamp(.85rem, 2vw, 1.35rem);
    }

    .ks-media-card {
      position: relative;
      display: flex;
      flex-direction: column;
      text-align: left;
      padding: 0;
      font: inherit;
      color: inherit;
      background: var(--ks-surface);
      border: 1px solid var(--ks-line);
      border-radius: var(--radius-lg);
      box-shadow: var(--ks-shadow-1);
      overflow: hidden;
      cursor: pointer;
      transition:
        box-shadow var(--duration-base) var(--ease-out),
        border-color var(--duration-base) var(--ease-out);
    }

    .ks-media-card:hover {
      box-shadow: var(--ks-shadow-2);
      border-color: var(--ks-accent);
    }

    .ks-media-card:focus-visible {
      outline: 2px solid var(--ks-focus-ring);
      outline-offset: 2px;
    }

    .ks-media-card-art {
      position: relative;
      aspect-ratio: 16 / 9;
      background: var(--ks-surface-sunken);
      background-size: cover;
      background-position: center;
      overflow: hidden;
    }

    /* A tournament with no artwork uploaded gets the band, not a grey rectangle.
       Flat --ks-surface-sunken at 16:9 reads as an image that failed to load,
       which on the match library meant every card looked broken until a coach
       had uploaded a background for it. The plate is the same navy the page
       opens with, so an empty slot reads as the product's own furniture. */
    .ks-media-card-art:not([data-media-state="canonical"]):not([data-media-state="legacy"]) {
      background:
        radial-gradient(75% 120% at 82% 0%, color-mix(in oklab, var(--accent) 16%, transparent) 0%, transparent 62%),
        linear-gradient(150deg, var(--ks-band-deep) 0%, var(--ks-band) 55%, var(--ks-band-raised) 100%);
      box-shadow: inset 0 -1px 0 var(--ks-band-edge);
    }

    /* Sits over the artwork so the count stays legible on any photograph, rather
       than depending on the photograph being dark. */
    .ks-media-card-badge {
      position: absolute;
      left: .6rem;
      bottom: .6rem;
      padding: .2rem .55rem;
      border-radius: var(--radius-full);
      background: rgba(4, 8, 15, .78);
      color: #FFFFFF;
      font-size: .6rem;
      font-weight: 800;
      letter-spacing: .1em;
      text-transform: uppercase;
    }

    .ks-media-card-body {
      display: flex;
      flex-direction: column;
      gap: .2rem;
      padding: .85rem .95rem 1rem;
      min-width: 0;
    }

    .ks-media-card-title {
      font-family: var(--ks-display);
      font-size: .95rem;
      font-weight: 700;
      line-height: 1.25;
      color: var(--ks-ink);
      /* Two lines, then an ellipsis: a long tournament name must not make one
         card twice the height of its neighbours. */
      display: -webkit-box;
      -webkit-line-clamp: 2;
      -webkit-box-orient: vertical;
      overflow: hidden;
    }

    .ks-media-card-meta {
      font-size: .72rem;
      color: var(--ks-ink-3);
    }

    /* Controls that belong to the card but are not the card's own action. They
       sit above it so a click on one never opens what the card opens. */
    .ks-media-card-tools {
      position: absolute;
      top: .5rem;
      right: .5rem;
      display: flex;
      gap: .3rem;
      z-index: 2;
    }

    .ks-media-card-tool {
      display: inline-flex;
      align-items: center;
      justify-content: center;
      min-width: 28px;
      height: 28px;
      padding: 0 .45rem;
      border: 1px solid rgba(255, 255, 255, .28);
      border-radius: var(--radius-md);
      background: rgba(4, 8, 15, .72);
      color: #FFFFFF;
      font-size: .72rem;
      line-height: 1;
      cursor: pointer;
    }

    .ks-media-card-tool:hover {
      background: rgba(4, 8, 15, .9);
      border-color: var(--ks-accent);
    }

    .ks-media-card-tool:focus-visible {
      outline: 2px solid var(--ks-focus-ring);
      outline-offset: 2px;
    }

    @media (prefers-reduced-motion: reduce) {
      .ks-media-card {
        transition: none;
      }
    }

    /* ══ TILE ═══════════════════════════════════════════════════════════════════
       A destination with a name and a sentence explaining it. Distinct from the
       quick links, which are a compact strip of six under a hero; a tile is what
       a page made of destinations is built from.
       ═══════════════════════════════════════════════════════════════════════════ */
    .ks-tilegrid {
      display: grid;
      grid-template-columns: repeat(auto-fill, minmax(14rem, 1fr));
      gap: clamp(.75rem, 2vw, 1.15rem);
    }

    .ks-tile {
      display: flex;
      flex-direction: column;
      gap: .3rem;
      padding: clamp(1rem, 2.5vw, 1.35rem);
      background: var(--ks-surface);
      border: 1px solid var(--ks-line);
      border-radius: var(--radius-lg);
      box-shadow: var(--ks-shadow-1);
      text-decoration: none;
      color: inherit;
      cursor: pointer;
      transition:
        box-shadow var(--duration-base) var(--ease-out),
        border-color var(--duration-base) var(--ease-out);
    }

    .ks-tile:hover {
      box-shadow: var(--ks-shadow-2);
      border-color: var(--ks-accent);
    }

    .ks-tile:focus-visible {
      outline: 2px solid var(--ks-focus-ring);
      outline-offset: 2px;
    }

    .ks-tile-icon {
      display: inline-flex;
      align-items: center;
      justify-content: center;
      width: 36px;
      height: 36px;
      margin-bottom: .35rem;
      font-size: 1.05rem;
      line-height: 1;
      background: var(--ks-surface-sunken);
      border-radius: var(--radius-md);
    }

    .ks-tile-title {
      font-family: var(--ks-display);
      font-size: .95rem;
      font-weight: 700;
      letter-spacing: .02em;
      color: var(--ks-ink);
    }

    .ks-tile-desc {
      font-size: .76rem;
      line-height: 1.5;
      color: var(--ks-ink-2);
    }

    @media (prefers-reduced-motion: reduce) {
      .ks-tile {
        transition: none;
      }
    }

    /* ══ FOOTER CONTACT ═════════════════════════════════════════════════════════
       Every row below sets a display, and a class rule beats the user agent's
       [hidden] { display: none }, so hidden would have had no effect on any of
       them -- an unfilled address showed as an empty line. Restated per element
       rather than as a blanket [hidden] !important, which would reach markup
       this file does not own.
       ═══════════════════════════════════════════════════════════════════════════ */
    .sf-contact li[hidden],
    .sf-socials[hidden],
    .sf-socials a[hidden],
    .ks-hero-kicker[hidden] {
      display: none;
    }

    .sf-contact {
      list-style: none;
      margin: .75rem 0 0;
      padding: 0;
      display: flex;
      flex-direction: column;
      gap: .3rem;
    }

    .sf-contact li {
      display: flex;
      align-items: flex-start;
      gap: .45rem;
      font-size: .82rem;
      line-height: 1.5;
      /* The address and hours are written as several lines by the dojo, so the
         line breaks it typed are the ones shown. */
      white-space: pre-line;
    }

    .sf-contact a {
      color: inherit;
      text-decoration: none;
    }

    .sf-contact a:hover {
      text-decoration: underline;
    }

    .sf-socials {
      display: flex;
      flex-wrap: wrap;
      gap: .5rem;
      margin-top: .75rem;
    }

    .sf-socials a {
      font-size: .64rem;
      font-weight: 700;
      letter-spacing: .1em;
      text-transform: uppercase;
      text-decoration: none;
      padding: .3rem .7rem;
      border: 1px solid currentColor;
      border-radius: var(--radius-full);
      opacity: .75;
    }

    .sf-socials a:hover {
      opacity: 1;
    }

    .ks-hero-kicker {
      display: block;
      font-size: .68rem;
      font-weight: 800;
      letter-spacing: .18em;
      text-transform: uppercase;
      color: var(--ks-band-accent);
      margin-bottom: .6rem;
    }

    /* ── Content history fold ──────────────────────────────────────────────
       Past and archived records, behind one closed disclosure per list. Only
       staff ever receive history from the renderers, so this is a management
       surface: quiet, counted, and never ahead of upcoming content. */
    /* A DISCLOSURE, WITH ITS ORIGINAL NAME KEPT ALIVE.
       These rules were written for one consumer — the "Past & archived (N)"
       fold on the Events page — and are entirely generic: a native <details>,
       a summary that carries its own chevron, a focus ring and a reduced-motion
       branch. A second admin surface needed exactly this, so the neutral name
       leads and .ks-content-history stays as an alias rather than being
       renamed out from under its three existing call sites. A third consumer
       should take .ks-disclosure. */
    .ks-disclosure,
    .ks-content-history {
      margin-top: 1.4rem;
      border: 1px solid var(--ks-line);
      border-radius: var(--radius-md);
      background: var(--ks-surface);
    }

    .ks-disclosure-summary,
    .ks-content-history-summary {
      cursor: pointer;
      list-style: none;
      display: flex;
      align-items: center;
      gap: .55rem;
      padding: .85rem 1.1rem;
      font-size: .74rem;
      font-weight: 800;
      letter-spacing: .08em;
      text-transform: uppercase;
      color: var(--ks-ink-2);
    }

    .ks-disclosure-summary::-webkit-details-marker,
    .ks-content-history-summary::-webkit-details-marker {
      display: none;
    }

    .ks-disclosure-summary::after,
    .ks-content-history-summary::after {
      content: '';
      width: 8px;
      height: 8px;
      border-right: 2px solid currentColor;
      border-bottom: 2px solid currentColor;
      transform: rotate(45deg);
      margin-left: auto;
      transition: transform var(--duration-fast) var(--ease-out);
    }

    .ks-disclosure[open] > .ks-disclosure-summary::after,
    .ks-content-history[open] > .ks-content-history-summary::after {
      transform: rotate(-135deg);
    }

    .ks-disclosure-summary:focus-visible,
    .ks-content-history-summary:focus-visible {
      outline: 2px solid var(--ks-focus-ring);
      outline-offset: 2px;
      border-radius: var(--radius-md);
    }

    .ks-disclosure-body,
    .ks-content-history-body {
      padding: 0 1.1rem 1.1rem;
      display: grid;
      gap: 1.1rem;
    }

    @media (prefers-reduced-motion: reduce) {
      .ks-disclosure-summary::after,
      .ks-content-history-summary::after {
        transition: none;
      }
    }

    /* Skip to content.
     *
     * Off-screen until focused, then a real, visible, high-contrast target —
     * the point is that a keyboard user SEES where the focus went, so it must
     * not be one of those "skip links" that stays invisible and leaves somebody
     * pressing Enter on nothing.
     *
     * Above everything: it is the first thing focused on the page and the
     * command dock is fixed, so a lower z-index would put the one control that
     * exists to be found underneath the thing it exists to skip. */
    .ks-skip-link {
      position: absolute;
      left: -9999px;
      top: 0;
      z-index: 10000;
      padding: .7rem 1.1rem;
      background: var(--ks-accent-strong);
      color: var(--ks-on-accent, #fff);
      font-weight: 700;
      font-size: .9rem;
      border-radius: 0 0 8px 0;
      text-decoration: none;
    }

    .ks-skip-link:focus,
    .ks-skip-link:focus-visible {
      left: 0;
      outline: 3px solid var(--ks-focus-ring, #fff);
      outline-offset: -3px;
    }

    /* The focused view must not draw a second, confusing ring around the whole
     * page: the skip link has already told the reader where they are, and the
     * content itself takes over from there. */
    .view.active:focus {
      outline: none;
    }


/* ══════════════════════════════════════════════════════════════════════
   ks-app-surface.v1.css
   ══════════════════════════════════════════════════════════════════════ */
/* ══════════════════════════════════════════════════════════════════════════
   THE AUTHENTICATED PRODUCT SURFACE — one component layer for every screen a
   signed-in member works on: My Dashboard, Training & Progress, the video
   detail and share sheet, and the Belt Requirements editor.

   WHY THIS FILE EXISTS. Those screens had grown four dialects: the Team Hub's
   --ksh-* tokens, the progression panel's --ks-prog-* aliases, seven button
   classes, five radius scales and labels down to .58rem — each surface
   styled locally, none of them reading the foundation the rest of the
   product reads. This layer consumes the foundation tokens ONLY
   (dojo-global-theme-tokens.v1.css: --ks-surface/--ks-line/--ks-ink,
   --ks-shadow-1..3, --ks-band-*, --ks-measure/--ks-gutter, the type roles,
   --ks-focus-ring, --duration-*/--ease-out) and defines the primitives the
   screens share. It declares no token of its own. A screen that needs a
   colour, a radius or a duration this file does not name is a screen that
   should ask the foundation, not invent one.

   THE CONTRACT, in the order a page is built:

     NAMING. This layer loads on every tenant page, so a class name here is
     global. Before adding one, grep dist/homann/index.html and the other
     stylesheets for it: `.ks-flag` was taken by the home hero's flag zone, and
     the first draft of the floating tag broke the homepage (2026-08-23).

     layout      .ks-app (+ --banded) · .ks-app-band · .ks-app-wrap · .ks-app-body
                 .ks-app-grid + .ks-col-N · .ks-rail
     surfaces    .ks-card (level 1) · .ks-panel (level 2) · the canvas (0)
                 .ks-card--primary · .ks-card--openable · .ks-card--inverse
     type        .ks-t-display · -title · -heading · -lead · -body · -meta ·
                 -label (the ONLY uppercase role) · -num
     actions     .ks-btn from the platform layer, plus -secondary · -quiet ·
                 -sm · -icon · -danger · -block declared here
     facts       .ks-kv · .ks-meta · .ks-chip · .ks-ribbon · .ks-status
     media       .ks-thumb · .ks-portrait
     forms       .ks-field · .ks-input · .ks-select · .ks-textarea · .ks-seg
     overlay     .ks-sheet

   THE TYPE FLOOR. Nothing on an authenticated surface is set below
   --ks-label (.72rem), and that role is for kickers and key labels only.
   Metadata is --ks-body-sm (.9rem). A card's title is sentence case at 1rem
   weight 800 — read at a glance, not decoded.

   THREE SURFACE LEVELS, and only three. The canvas; a card, which is raised
   by --ks-shadow-1 and held by --ks-line; a panel inside a card, which is
   sunken and held by --ks-line-soft. --ks-shadow-2 is the hover of an
   openable card and the weight of a primary-tier card; --ks-shadow-3 is the
   sheet. Nothing else casts a shadow.

   MOTION. Every transition is --duration-fast or --duration-base on
   --ease-out; the signature lift belongs only to a card that opens.
   ══════════════════════════════════════════════════════════════════════════ */

/* ── layout ───────────────────────────────────────────────────────────────── */

.ks-app {
  background: var(--ks-canvas);
  color: var(--ks-ink);
  font-family: var(--ks-text);
  font-size: var(--ks-body);
  line-height: 1.5;
}

/* The app's band is the platform's band (.ks-band paints it), with two
   differences a product header needs: its content may break its foot line,
   and it carries the nav allowance itself. */
.ks-app-band {
  position: relative;
  isolation: isolate;
  overflow: visible;
  padding-top: var(--ks-nav-h);
  color: var(--ks-band-ink);
}
.ks-app-band .ks-app-band-photo {
  position: absolute;
  inset: 0;
  z-index: -2;
  overflow: hidden;
}
.ks-app-band .ks-app-band-photo img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: center 28%;
  opacity: .28;
}
.ks-app-band .ks-app-band-photo img[data-media-state="empty"],
.ks-app-band .ks-app-band-photo img[data-media-state="error"] { display: none; }
/* A view that opens with a band pays no second nav allowance. */
/* A banded page (.ks-app--banded on its root) carries the nav allowance in
   its band, so the view shell's allowance is switched off — once, here,
   rather than per page. The structural form is kept for a band placed
   directly under the view. */
.view.active:has(> .ks-app--banded),
.view.active:has(> .ks-app-band:first-child) { padding-top: 0; }

.ks-app-wrap {
  width: 100%;
  max-width: var(--ks-measure);
  margin: 0 auto;
  padding-inline: var(--ks-gutter);
}
.ks-app-wrap--narrow { max-width: var(--ks-measure-narrow, 760px); }
.ks-app-body {
  padding-block: clamp(1.25rem, 2.4vw, 2rem) clamp(2.5rem, 5vw, 4rem);
}

/* Twelve columns, three breakpoints, no per-module widths. A module says how
   many columns it takes; the grid says how that collapses. */
.ks-app-grid {
  display: grid;
  grid-template-columns: repeat(12, minmax(0, 1fr));
  gap: clamp(.9rem, 1.6vw, 1.25rem);
  align-items: stretch;
}
.ks-col-3 { grid-column: span 3; }
.ks-col-4 { grid-column: span 4; }
.ks-col-5 { grid-column: span 5; }
.ks-col-6 { grid-column: span 6; }
.ks-col-7 { grid-column: span 7; }
.ks-col-8 { grid-column: span 8; }
.ks-col-12 { grid-column: span 12; }
@media (max-width: 959px) {
  .ks-col-3, .ks-col-4, .ks-col-5, .ks-col-6, .ks-col-7, .ks-col-8 { grid-column: span 6; }
  /* A module that should take the whole width once the grid is two across. */
  .ks-col-md-12 { grid-column: span 12; }
}
@media (max-width: 639px) {
  .ks-col-3, .ks-col-4, .ks-col-5, .ks-col-6, .ks-col-7, .ks-col-8 { grid-column: span 12; }
}

/* A rail: a grid of equal tiles from 640px, a horizontal snap strip below —
   the right gesture for film on a phone. */
.ks-rail {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(min(100%, var(--ks-rail-min, 14rem)), 1fr));
  gap: .75rem;
}
@media (max-width: 639px) {
  .ks-rail {
    grid-template-columns: none;
    grid-auto-flow: column;
    grid-auto-columns: minmax(11.5rem, 72%);
    overflow-x: auto;
    scroll-snap-type: x proximity;
    padding-bottom: .3rem;
    scrollbar-width: thin;
  }
  .ks-rail > * { scroll-snap-align: start; }
}

/* ── surfaces ─────────────────────────────────────────────────────────────── */

.ks-card {
  position: relative;
  display: flex;
  flex-direction: column;
  min-width: 0;
  background: var(--ks-surface);
  border: 1px solid var(--ks-line);
  border-radius: var(--ks-r-lg);
  box-shadow: var(--ks-shadow-1);
  overflow: hidden;
}
/* The primary tier: one step more weight, and the tenant's hairline along
   the top — the same edge the band's foot wears. */
.ks-card--primary {
  box-shadow: var(--ks-shadow-2);
}
/* Absolutely placed so it is never a flex or grid item of the card: a strip
   laid out as a grid of cells must not find a fourth, invisible cell. */
.ks-card--primary::before {
  content: '';
  position: absolute;
  inset: 0 0 auto 0;
  height: 3px;
  background: linear-gradient(90deg, var(--ks-accent), var(--ks-accent-strong));
  pointer-events: none;
}
.ks-card--primary > .ks-card-head:first-child { padding-top: calc(.9rem + 3px); }
.ks-card--primary > .ks-card-body:first-child { padding-top: calc(1rem + 3px); }
/* The supporting tier: no lift, a soft line, a quieter head. Resources,
   coaching snippets, the dojo's note, the belt journey. */
.ks-card--quiet { box-shadow: none; border-color: var(--ks-line-soft); }
.ks-card--quiet > .ks-card-head { padding-block: .75rem; }
.ks-card--quiet > .ks-card-head .ks-card-title { font-weight: 700; color: var(--ks-ink-2); font-size: .95rem; }
.ks-card--quiet > .ks-card-head .ks-icon { color: var(--ks-ink-3); }
/* A card the reader can open: the product's one signature interaction. */
.ks-card--openable {
  cursor: pointer;
  transition:
    box-shadow var(--duration-base) var(--ease-out),
    border-color var(--duration-base) var(--ease-out),
    transform var(--duration-base) var(--ease-out);
}
.ks-card--openable:hover {
  box-shadow: var(--ks-shadow-2);
  border-color: var(--ks-accent);
  transform: translateY(-2px);
}
/* A feature cover inside a card: the band's own dark, the countdown pinned
   top-right, the title and the date over it. For the one feature per page
   that earns it. */
.ks-cover {
  position: relative;
  display: grid;
  gap: .3rem;
  padding: 1.1rem 1.15rem 1.15rem;
  padding-right: clamp(7rem, 20%, 9rem);
  background:
    radial-gradient(96% 150% at 4% 112%, var(--ks-band-accent-soft) 0%, transparent 56%),
    radial-gradient(80% 120% at 98% 0%, var(--ks-band-red) 0%, transparent 58%),
    linear-gradient(150deg, var(--ks-band-deep) 0%, var(--ks-band) 56%, var(--ks-band-raised) 100%);
  color: var(--ks-band-ink);
  box-shadow: var(--ks-sheen-dark);
}
.ks-cover .ks-t-title, .ks-cover .ks-t-heading { color: var(--ks-band-ink); }
.ks-cover .ks-t-lead, .ks-cover .ks-t-meta { color: var(--ks-band-ink-2); }
.ks-cover-count { position: absolute; top: 1rem; right: 1rem; }

/* A card painted in the band's own dark, for the one feature per page that
   earns it. */
.ks-card--inverse {
  background:
    radial-gradient(96% 150% at 4% 112%, var(--ks-band-accent-soft) 0%, transparent 56%),
    radial-gradient(80% 120% at 98% 0%, var(--ks-band-red) 0%, transparent 58%),
    linear-gradient(150deg, var(--ks-band-deep) 0%, var(--ks-band) 56%, var(--ks-band-raised) 100%);
  color: var(--ks-band-ink);
  border-color: var(--ks-band-line);
}

.ks-card-head {
  display: flex;
  align-items: center;
  gap: .6rem;
  padding: .9rem 1.15rem;
  border-bottom: 1px solid var(--ks-line-soft);
}
.ks-card-head .ks-icon { width: 1.1rem; height: 1.1rem; color: var(--ks-accent-strong); }
.ks-card-title {
  margin: 0;
  font-family: var(--ks-display);
  font-size: 1rem;
  font-weight: 800;
  letter-spacing: -.005em;
  line-height: 1.25;
  color: var(--ks-ink);
}
.ks-card-action { margin-left: auto; flex-shrink: 0; }
/* On a phone the head keeps its title whole and lets the action drop to a
   second line, right-aligned, when the two cannot share one. */
@media (max-width: 639px) {
  .ks-card-head { flex-wrap: wrap; }
  /* The head's action is set smaller on a phone so title and action share
     one line where a tablet would have had room anyway. */
  .ks-card-action.ks-btn-sm { font-size: .7rem; letter-spacing: .04em; padding-inline: .45rem; }
}
.ks-card-body { padding: 1rem 1.15rem 1.15rem; display: grid; gap: .5rem; }
/* A chip or a button placed directly in a body is content-sized, never
   stretched to the column. */
.ks-card-body > .ks-chip, .ks-card-body > .ks-btn { justify-self: start; }
.ks-card-body--flush { padding: 0; }
.ks-card-foot {
  margin-top: auto;
  padding: .85rem 1.15rem 1.1rem;
  display: flex;
  flex-wrap: wrap;
  gap: .5rem;
}
/* A quiet note that closes a card: "+3 more in Training & Progress". */
.ks-card-note { margin: 0; padding: 0 1.15rem 1rem; font-size: var(--ks-body-sm); color: var(--ks-ink-3); }

.ks-panel {
  background: var(--ks-surface-sunken);
  border: 1px solid var(--ks-line-soft);
  border-radius: var(--ks-r-md);
}

/* ── type roles ───────────────────────────────────────────────────────────── */

.ks-t-display {
  margin: 0;
  font-family: var(--ks-display);
  font-size: var(--ks-display-1);
  font-weight: var(--ks-weight-display);
  line-height: 1.02;
  letter-spacing: -.022em;
  text-wrap: balance;
}
.ks-t-title {
  margin: 0;
  font-family: var(--ks-display);
  font-size: var(--ks-title-1);
  font-weight: var(--ks-weight-title);
  line-height: 1.1;
  letter-spacing: -.012em;
  text-wrap: balance;
}
.ks-t-heading {
  margin: 0;
  font-family: var(--ks-display);
  font-size: var(--ks-title-2);
  font-weight: var(--ks-weight-title);
  line-height: 1.2;
  letter-spacing: -.008em;
}
.ks-t-lead { margin: 0; font-size: var(--ks-body-lg); line-height: 1.55; color: var(--ks-ink-2); }
.ks-t-body { margin: 0; font-size: var(--ks-body); line-height: 1.5; }
.ks-t-meta { margin: 0; font-size: var(--ks-body-sm); line-height: 1.45; color: var(--ks-ink-2); }
.ks-t-label {
  margin: 0;
  font-size: var(--ks-label);
  font-weight: 800;
  letter-spacing: .1em;
  text-transform: uppercase;
  color: var(--ks-ink-3);
}
.ks-t-num {
  font-family: var(--ks-display);
  font-weight: 700;
  font-variant-numeric: tabular-nums;
  letter-spacing: -.01em;
  line-height: 1;
}
/* On a band, the same roles in the band's inks. */
.ks-app-band .ks-t-lead, .ks-card--inverse .ks-t-lead { color: var(--ks-band-ink-2); }
.ks-app-band .ks-t-meta, .ks-card--inverse .ks-t-meta { color: var(--ks-band-ink-2); }
.ks-app-band .ks-t-label, .ks-card--inverse .ks-t-label { color: var(--ks-band-accent); }

/* The kicker over a band title: the platform's eyebrow. */
.ks-kicker {
  display: inline-flex;
  align-items: center;
  gap: .6rem;
  margin: 0;
  font-size: var(--ks-label);
  font-weight: 800;
  letter-spacing: .18em;
  text-transform: uppercase;
  color: var(--ks-band-accent);
}
.ks-kicker::before {
  content: '';
  width: 26px;
  height: 2px;
  background: linear-gradient(90deg, var(--primary), var(--ks-band-accent));
}

/* ── actions: the platform button, extended ───────────────────────────────── */

.ks-btn-secondary {
  background: var(--ks-surface);
  border-color: var(--ks-line-strong);
  color: var(--ks-ink);
}
.ks-btn-secondary:hover {
  border-color: var(--ks-accent);
  color: var(--ks-accent-strong);
}
.ks-btn-quiet {
  padding: .5rem .35rem;
  background: transparent;
  border-color: transparent;
  color: var(--ks-accent-strong);
}
.ks-btn-quiet:hover { text-decoration: underline; text-underline-offset: .2em; }
.ks-btn-sm {
  padding: .6rem 1rem;
  font-size: .72rem;
}
.ks-btn-icon {
  width: 2.4rem;
  height: 2.4rem;
  padding: 0;
  border-radius: var(--ks-r-control);
  background: var(--ks-surface);
  border-color: var(--ks-line-strong);
  color: var(--ks-ink-2);
}
.ks-btn-icon:hover { border-color: var(--ks-accent); color: var(--ks-accent-strong); }
.ks-btn-icon .ks-icon { width: 1.1rem; height: 1.1rem; }
/* Outlined, in danger's own hue, with its mark: the platform's rule for the
   button that must never be confused with the primary one. */
.ks-btn-danger {
  background: var(--ks-danger-bg);
  border-color: var(--ks-danger-line);
  color: var(--ks-danger);
}
.ks-btn-danger:hover { background: color-mix(in oklab, var(--ks-danger) 14%, var(--ks-danger-bg)); }
.ks-btn-block { width: 100%; }
/* THE [hidden] GUARD IS NOT DECORATION. .ks-btn sets its own display, which
   out-specifies the UA's `[hidden] { display: none }` — the same trap the
   admin calendar and the shipped-surfaces file document. Scripts hide these
   buttons with the `hidden` attribute (the footage lock hides Paste Link);
   without this line the attribute does nothing. Per-selector, no !important,
   per the codebase's own doctrine. */
.ks-btn[hidden] { display: none; }
.ks-btn[disabled], .ks-btn:disabled { opacity: .55; cursor: not-allowed; transform: none; }
/* A finger is not a cursor: on a touch pointer the quiet action keeps its
   label and gains a real target. */
@media (pointer: coarse) {
  .ks-btn-quiet { padding: .75rem .45rem; }
}

/* ── facts ────────────────────────────────────────────────────────────────── */

/* A labelled value. The label is the one uppercase role; the value is body
   weight 700 — the thing the eye lands on. */
.ks-kv { display: grid; gap: .15rem; min-width: 0; }
.ks-kv-k { font-size: var(--ks-label); font-weight: 800; letter-spacing: .08em; text-transform: uppercase; color: var(--ks-ink-3); }
.ks-kv-v { font-size: var(--ks-body); font-weight: 700; line-height: 1.3; color: var(--ks-ink); overflow-wrap: anywhere; }
.ks-kv-s { font-size: var(--ks-body-sm); color: var(--ks-ink-3); }
.ks-kv--lg .ks-kv-v { font-family: var(--ks-display); font-size: var(--ks-title-1); font-weight: 700; line-height: 1.1; }
.ks-card--inverse .ks-kv-k, .ks-app-band .ks-kv-k { color: var(--ks-band-ink-3); }
.ks-card--inverse .ks-kv-v, .ks-app-band .ks-kv-v { color: var(--ks-band-ink); }
.ks-card--inverse .ks-kv-s, .ks-app-band .ks-kv-s { color: var(--ks-band-ink-2); }
/* A light card seated inside a band (the dashboard's rank strip) keeps its
   own ink: the band's inverse roles stop at the card's edge. */
.ks-app-band .ks-card:not(.ks-card--inverse) { color: var(--ks-ink); }
.ks-app-band .ks-card:not(.ks-card--inverse) .ks-kv-k { color: var(--ks-ink-3); }
.ks-app-band .ks-card:not(.ks-card--inverse) .ks-kv-v { color: var(--ks-ink); }
.ks-app-band .ks-card:not(.ks-card--inverse) .ks-kv-s { color: var(--ks-ink-2); }
.ks-app-band .ks-card:not(.ks-card--inverse) :is(.ks-t-lead, .ks-t-meta) { color: var(--ks-ink-2); }
.ks-app-band .ks-card:not(.ks-card--inverse) .ks-t-label { color: var(--ks-ink-3); }

/* An inline list of facts, dot-separated, at metadata size. */
.ks-meta {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: .25rem .55rem;
  margin: 0;
  padding: 0;
  list-style: none;
  font-size: var(--ks-body-sm);
  color: var(--ks-ink-2);
}
.ks-meta > * + *::before { content: '·'; margin-right: .55rem; color: var(--ks-ink-3); }
.ks-meta strong { color: var(--ks-ink); font-weight: 700; }

/* The one chip. */
.ks-chip {
  display: inline-flex;
  align-items: center;
  gap: .35rem;
  text-decoration: none;
  padding: .25rem .65rem;
  border-radius: var(--ks-r-pill);
  border: 1px solid var(--ks-line);
  background: var(--ks-surface);
  color: var(--ks-ink-2);
  font-size: var(--ks-body-sm);
  font-weight: 700;
  line-height: 1.3;
  white-space: nowrap;
  max-width: 100%;
}
.ks-chip--accent { background: var(--ks-accent-soft); border-color: color-mix(in oklab, var(--ks-accent) 40%, transparent); color: var(--ks-accent-strong); }
.ks-chip--ok { background: var(--ks-success-bg); border-color: var(--ks-success-line); color: var(--ks-success); }
.ks-chip--info { background: var(--ks-info-bg); border-color: var(--ks-info-line); color: var(--ks-info); }
.ks-chip--warn { background: var(--ks-warning-bg); border-color: var(--ks-warning-line); color: var(--ks-warning); }
.ks-chip--inverse { background: var(--ks-band-deep); border-color: var(--ks-band-deep); color: var(--ks-band-ink); }
.ks-chip--sunken { background: var(--ks-surface-sunken); border-color: var(--ks-line-soft); }
.ks-chip--sm { font-size: var(--ks-label); padding: .18rem .5rem; }
.ks-chip .ks-chip-n { font-variant-numeric: tabular-nums; opacity: .7; }
/* A chip that is a choice: the one that is chosen fills. */
.ks-chip--toggle { appearance: none; cursor: pointer; font: inherit; font-size: var(--ks-body-sm); font-weight: 700; }
.ks-chip--toggle:hover { border-color: var(--ks-ink-3); color: var(--ks-ink); }
.ks-chip--toggle[aria-selected="true"], .ks-chip--toggle[aria-expanded="true"], .ks-chip--toggle.is-on { background: var(--ks-ink); border-color: var(--ks-ink); color: var(--ks-ink-inverse); }
/* A state mark beside a name — neutral until something has happened. */
.ks-chip-mark { width: 8px; height: 8px; border-radius: 50%; background: var(--ks-line-strong); flex: 0 0 auto; }
.ks-chip-mark--ok { background: var(--ks-success); }
.ks-chip-mark--info { background: var(--ks-info); }
.ks-chip-mark--accent { background: var(--ks-accent); }
.ks-chip--ok .ks-chip-mark { background: var(--ks-success); }
.ks-chip--info .ks-chip-mark { background: var(--ks-info); }

/* A corner flag on a thumbnail: the thing that is new. */
.ks-ribbon {
  position: absolute;
  right: .5rem;
  top: .5rem;
  padding: .2rem .55rem;
  border-radius: var(--ks-r-pill);
  background: var(--ks-accent);
  color: var(--ks-on-accent);
  font-size: var(--ks-label);
  font-weight: 800;
  letter-spacing: .04em;
  box-shadow: var(--ks-shadow-2);
}

/* A line of status under a control: what just happened. */
.ks-status { margin: 0; min-height: 1.3em; font-size: var(--ks-body-sm); color: var(--ks-ink-2); }
.ks-status.is-error { color: var(--ks-danger); }
.ks-status:empty { display: none; }

/* A quiet sentence where a module has one true thing to say. */
.ks-note { margin: 0; font-size: var(--ks-body-sm); line-height: 1.5; color: var(--ks-ink-2); }

/* ── media ────────────────────────────────────────────────────────────────── */

/* A 16:9 plate. With no image it is the band, not a grey rectangle — the
   same rule the Match Library's cards keep. */
.ks-thumb {
  position: relative;
  display: block;
  aspect-ratio: 16 / 9;
  overflow: hidden;
  background:
    radial-gradient(75% 120% at 82% 0%, var(--ks-band-accent-soft) 0%, transparent 62%),
    linear-gradient(150deg, var(--ks-band-deep) 0%, var(--ks-band) 55%, var(--ks-band-raised) 100%);
}
.ks-thumb img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; display: block; transition: transform var(--duration-base) var(--ease-out); }
.ks-card--openable:hover .ks-thumb img { transform: scale(1.04); }
.ks-thumb-glyph {
  position: absolute;
  inset: 0;
  display: grid;
  place-items: center;
  color: var(--ks-band-ink);
  opacity: .92;
  filter: drop-shadow(0 2px 8px color-mix(in oklab, var(--ks-band-deep) 60%, transparent));
}
.ks-thumb-glyph .ks-icon { width: 2rem; height: 2rem; }
.ks-thumb-tag {
  position: absolute;
  left: .5rem;
  top: .5rem;
  padding: .2rem .55rem;
  border-radius: var(--ks-r-pill);
  background: color-mix(in oklab, var(--ks-band-deep) 74%, transparent);
  color: var(--ks-band-ink);
  font-size: var(--ks-label);
  font-weight: 800;
  letter-spacing: .06em;
  backdrop-filter: blur(4px);
}
.ks-thumb-tag--accent { background: var(--ks-accent); color: var(--ks-on-accent); }

/* The athlete's portrait: a plate, not an avatar in a circle. The belt
   colour is the ring, a hairline sits inside it, a floor gradient lets the
   band read through the photograph, and the monogram is the finished empty
   state. --ks-portrait-belt is set per athlete by the renderer. */
.ks-portrait {
  position: relative;
  width: var(--ks-portrait-w, 200px);
  aspect-ratio: 4 / 5;
  border-radius: var(--ks-r-xl);
  padding: 3px;
  background: linear-gradient(150deg,
    color-mix(in srgb, var(--ks-portrait-belt, var(--ks-band-accent)) 85%, transparent),
    color-mix(in oklab, var(--ks-band-ink) 18%, transparent) 42%,
    color-mix(in srgb, var(--ks-portrait-belt, var(--ks-band-accent)) 55%, transparent));
  box-shadow: var(--ks-e-dark);
}
.ks-portrait::after {
  content: '';
  position: absolute;
  inset: 3px;
  border-radius: calc(var(--ks-r-xl) - 3px);
  box-shadow: inset 0 0 0 1px color-mix(in oklab, var(--ks-band-ink) 22%, transparent);
  pointer-events: none;
}
.ks-portrait-plate {
  position: relative;
  width: 100%;
  height: 100%;
  border-radius: calc(var(--ks-r-xl) - 3px);
  overflow: hidden;
  display: grid;
  place-items: center;
  background: linear-gradient(165deg, var(--ks-band-raised), var(--ks-band-deep));
  color: var(--ks-band-accent);
  font-family: var(--ks-display);
  font-size: clamp(2rem, 4vw, 3rem);
  font-weight: 700;
  letter-spacing: .04em;
}
.ks-portrait-img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; object-position: center 22%; }
.ks-portrait-plate::after {
  content: '';
  position: absolute;
  inset: 0;
  background: linear-gradient(to top,
    color-mix(in oklab, var(--ks-band-deep) 78%, transparent) 0%,
    color-mix(in oklab, var(--ks-band-deep) 18%, transparent) 34%,
    transparent 62%);
  pointer-events: none;
}
.ks-portrait-img + .ks-portrait-initials { opacity: 0; }

/* ── forms ────────────────────────────────────────────────────────────────── */

.ks-field { display: grid; gap: .35rem; min-width: 0; margin: 0; padding: 0; border: 0; }
.ks-field-label { font-size: var(--ks-body-sm); font-weight: 700; color: var(--ks-ink); }
.ks-field-hint { font-size: var(--ks-body-sm); color: var(--ks-ink-3); line-height: 1.45; }
.ks-input, .ks-select, .ks-textarea {
  appearance: none;
  -webkit-appearance: none;
  width: 100%;
  min-width: 0;
  box-sizing: border-box;
  padding: .65rem .8rem;
  border: 1px solid var(--ks-line-strong);
  border-radius: var(--ks-r-sm);
  background: var(--ks-surface);
  color: var(--ks-ink);
  font: inherit;
  font-size: var(--ks-body);
  line-height: 1.35;
  transition: border-color var(--duration-fast) var(--ease-out), box-shadow var(--duration-fast) var(--ease-out);
}
.ks-input::placeholder, .ks-textarea::placeholder { color: var(--ks-ink-3); }
.ks-textarea { resize: vertical; min-height: 3.4rem; }
.ks-select {
  padding-right: 2.2rem;
  background-image: linear-gradient(45deg, transparent 50%, currentColor 50%), linear-gradient(135deg, currentColor 50%, transparent 50%);
  background-position: calc(100% - 1.05rem) 50%, calc(100% - .7rem) 50%;
  background-size: .35rem .35rem, .35rem .35rem;
  background-repeat: no-repeat;
}
.ks-input:focus-visible, .ks-select:focus-visible, .ks-textarea:focus-visible {
  outline: none;
  border-color: var(--ks-accent);
  box-shadow: 0 0 0 3px var(--ks-accent-soft);
}
.ks-input--sm, .ks-select--sm { padding: .45rem .65rem; font-size: var(--ks-body-sm); }
.ks-input[disabled], .ks-select[disabled], .ks-textarea[disabled] { opacity: .6; cursor: not-allowed; }

/* A segmented choice: radios dressed as chips; the chosen one fills. */
.ks-seg { display: flex; flex-wrap: wrap; gap: .4rem; }
.ks-seg-item {
  position: relative;
  display: inline-flex;
  align-items: center;
  padding: .5rem .95rem;
  border-radius: var(--ks-r-pill);
  border: 1px solid var(--ks-line-strong);
  background: var(--ks-surface);
  font-size: var(--ks-body-sm);
  font-weight: 700;
  cursor: pointer;
  transition: background var(--duration-fast) var(--ease-out), border-color var(--duration-fast) var(--ease-out);
}
.ks-seg-item input { position: absolute; inset: 0; opacity: 0; margin: 0; cursor: pointer; }
.ks-seg-item:has(input:checked) { background: var(--ks-ink); border-color: var(--ks-ink); color: var(--ks-ink-inverse); }
.ks-seg-item:has(input:focus-visible) { outline: 2px solid var(--ks-focus-ring); outline-offset: 2px; }

/* ── the sheet ────────────────────────────────────────────────────────────── */

/* One native <dialog>, in the top layer over the page. A drawer from the
   right on a desktop; the whole screen on a phone. */
dialog.ks-sheet {
  position: fixed;
  inset: 0 0 0 auto;
  width: min(640px, 100vw);
  height: 100dvh;
  max-height: 100dvh;
  max-width: 100vw;
  margin: 0;
  padding: 0;
  border: 0;
  background: var(--ks-canvas);
  color: var(--ks-ink);
  font-family: var(--ks-text);
  box-shadow: var(--ks-shadow-3);
  overflow: hidden auto;
  overscroll-behavior: contain;
}
dialog.ks-sheet[open] { animation: ksSheetIn var(--duration-base) var(--ease-out); }
dialog.ks-sheet::backdrop { background: color-mix(in oklab, var(--ks-band-deep) 52%, transparent); backdrop-filter: blur(2px); }
html.ks-sheet-open { overflow: hidden; }
@keyframes ksSheetIn { from { translate: 32px 0; opacity: .6; } to { translate: 0 0; opacity: 1; } }
.ks-sheet-panel { display: grid; gap: 1rem; padding: 1.1rem 1.25rem 1.5rem; }
.ks-sheet-head { display: flex; align-items: flex-start; gap: 1rem; }
.ks-sheet-titles { display: grid; gap: .4rem; min-width: 0; flex: 1; }
.ks-sheet-close {
  flex: 0 0 auto;
  width: 2.5rem;
  height: 2.5rem;
  border-radius: 50%;
}
@media (max-width: 639px) {
  dialog.ks-sheet { inset: 0; width: 100vw; box-shadow: none; }
  dialog.ks-sheet[open] { animation-name: ksSheetUp; }
  @keyframes ksSheetUp { from { translate: 0 24px; opacity: .6; } to { translate: 0 0; opacity: 1; } }
  .ks-sheet-panel { padding: .9rem 1rem calc(1.5rem + env(safe-area-inset-bottom, 0px)); }
}

/* ── the conversation ─────────────────────────────────────────────────────
   The player, the thread and the composer (ks-coaching-thread.v1.js), drawn
   the same in the Training & Progress drawer and in the video sheet. */
.ks-embed {
  position: relative;
  width: 100%;
  aspect-ratio: 16 / 9;
  border-radius: var(--ks-r-md);
  overflow: hidden;
  background: var(--ks-band-deep);
}
.ks-embed iframe { position: absolute; inset: 0; width: 100%; height: 100%; border: 0; }

.ks-thread { list-style: none; margin: 0; padding: 0; display: grid; gap: .5rem; }
.ks-thread-item {
  padding: .7rem .85rem;
  border-radius: var(--ks-r-md);
  background: var(--ks-surface);
  border: 1px solid var(--ks-line-soft);
}
.ks-thread-item.is-coach { border-left: 3px solid var(--ks-accent); }
.ks-thread-who { display: flex; align-items: center; gap: .5rem; flex-wrap: wrap; font-size: var(--ks-body-sm); }
.ks-thread-name { font-weight: 800; color: var(--ks-ink); }
.ks-thread-when { margin-left: auto; color: var(--ks-ink-3); }
.ks-thread-text { margin: .3rem 0 0; font-size: var(--ks-body); line-height: 1.5; white-space: pre-line; }

.ks-composer { display: grid; gap: .45rem; margin-top: .6rem; }
.ks-composer-row { display: flex; gap: .45rem; align-items: center; }
.ks-composer-row .ks-composer-time { width: 6rem; font-variant-numeric: tabular-nums; }
.ks-composer-row .ks-btn { margin-left: auto; }

/* ── focus, everywhere on the surface ────────────────────────────────────── */
.ks-app :is(a, button, summary, [tabindex]):focus-visible,
dialog.ks-sheet :is(a, button, summary, [tabindex]):focus-visible {
  outline: 2px solid var(--ks-focus-ring);
  outline-offset: 2px;
  border-radius: var(--ks-r-control-sm);
}
.ks-app-band :is(a, button, summary, [tabindex]):focus-visible { outline-color: var(--ks-band-accent); }

@media (prefers-reduced-motion: reduce) {
  .ks-card--openable, .ks-thumb img, .ks-seg-item, .ks-input, .ks-select, .ks-textarea { transition: none; }
  .ks-card--openable:hover { transform: none; }
  .ks-card--openable:hover .ks-thumb img { transform: none; }
  dialog.ks-sheet[open] { animation: none; }
}

/* ── VIDEO REVIEW — the one thread ─────────────────────────────────────────
   The shared renderer's own classes (ks-video-review-thread.v1.js), living
   HERE because they EXTEND the conversation anatomy above (.ks-thread,
   .ks-thread-item, .is-coach) and must cascade immediately after it. They
   were briefly a separate stylesheet; the consolidator then had to hoist this
   whole file into an earlier bundle to keep the pair ordered, and that
   re-pinning flipped unrelated cascade ties — measured as the phone hub band
   growing 7px past its budgeted 245px. One file, one position, no hoist.

   One restrained hierarchy instead of the Library's four signals per row:
   the staff accent is the thin left edge the Dashboard already draws, the
   role chip appears only when it says something the name does not, and the
   actions are quiet text — always visible, because hover does not exist on
   the phones half these readers hold. */

.ks-review-root { display: grid; gap: .5rem; }

/* ── the finishing pass ───────────────────────────────────────────────────
   A conversation is ONE surface, not a stack of cards. Every row used to be an
   outlined box painted the same colour as the card holding it, so five
   comments drew six borders and the only thing separating them was a line —
   which in dark meant no surface hierarchy at all, the row and its card being
   the identical fill. The rows lose the box and keep the rhythm: a hairline
   between siblings, space either side of it, and the card's own border as the
   only frame. That also hands each row its full width back, which is what pays
   for the author's mark on a phone.

   Nothing here changes what is rendered, who may act, or what anything says.
   Every rule below is a paint. */
.ks-review-list > .ks-review-item {
  padding: .85rem 0;
  border: 0;
  border-radius: 0;
  background: none;
}
/* Between siblings only — never above the first row, which already sits under
   the composer's own separation. */
.ks-review-list > .ks-review-item + .ks-review-item {
  border-top: 1px solid var(--ks-line-soft);
}
.ks-review-list { gap: 0; }

/* The staff bar is gone rather than quieted. It repeated on most rows, so it
   stopped separating anything, and it said what the role chip beside the name
   already says in words — with the author's mark now carrying identity, it was
   the third signal for one fact. `.is-coach` stays on the element: the parity
   spec reads it, and it is still the honest hook for a future treatment. */
.ks-review-item.is-coach { border-left: 0; }

/* The row states its own ink. The anatomy classes inherit colour, which was
   right inside the app surface (an ancestor sets --ks-ink there) and wrong on
   the Match Library page, whose old notes container painted everything muted
   grey — comment BODIES rendered as whispers. The body is the point of the
   row; it gets the ink explicitly. */
.ks-review-item { color: var(--ks-ink); }

/* ── who is speaking, at a glance ─────────────────────────────────────────
   The row was one column and the NAME was the only per-person signal, so a
   thread carrying two coaches read as one voice until you stopped and checked
   each line. The mark is a monogram tinted from the author's key; the rest of
   the row moves into a second column beside it, which is also what gives a
   reply rail something to align under.

   The hue arrives inline as --ks-review-hue (twelve stops, 30 degrees apart,
   hashed from the author's uid — same person, same colour, everywhere). ONLY
   the hue: lightness is pinned here, so no author can be assigned an
   unreadable mark. The pairs are chosen against the WORST hue rather than a
   comfortable one — yellow, at 60 — and clear 7:1 there in both themes, so
   all twelve are AAA and none is merely passing. theme-contrast.spec.ts walks
   these rendered nodes, so a later nudge to the lightness is caught. */
.ks-review-item {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr);
  column-gap: .6rem;
  align-items: start;
}
.ks-review-item > :not(.ks-review-avatar) { grid-column: 2; }

.ks-review-avatar {
  --ks-review-hue: 220;
  --ks-review-mark-ink: hsl(var(--ks-review-hue) 75% 18%);
  --ks-review-mark-bg: hsl(var(--ks-review-hue) 72% 84%);
  --ks-review-mark-line: hsl(var(--ks-review-hue) 55% 66%);
  grid-column: 1;
  grid-row: 1;
  width: 1.75rem;
  height: 1.75rem;
  border-radius: 50%;
  display: grid;
  place-items: center;
  font-size: var(--ks-label);
  font-weight: 800;
  letter-spacing: .01em;
  line-height: 1;
  user-select: none;
  color: var(--ks-review-mark-ink);
  background: var(--ks-review-mark-bg);
  border: 1px solid var(--ks-review-mark-line);
}

/* Same twelve hues, inverted about the surface — a light disc on a dark card
   is a headlamp, so the ink and ground swap rather than the hue changing. Only
   the three slots move; the geometry above is written once. The pair of blocks
   is this file's established shape: an explicit choice wins, and a reader who
   asked their OS for dark and chose nothing here gets dark. */
:root[data-theme="dark"] .ks-review-avatar {
  --ks-review-mark-ink: hsl(var(--ks-review-hue) 70% 88%);
  --ks-review-mark-bg: hsl(var(--ks-review-hue) 32% 24%);
  --ks-review-mark-line: hsl(var(--ks-review-hue) 42% 44%);
}
@media (prefers-color-scheme: dark) {
  :root:not([data-theme]) .ks-review-avatar {
    --ks-review-mark-ink: hsl(var(--ks-review-hue) 70% 88%);
    --ks-review-mark-bg: hsl(var(--ks-review-hue) 32% 24%);
    --ks-review-mark-line: hsl(var(--ks-review-hue) 42% 44%);
  }
}

/* The timestamp is still the row's one primary control and still accent ink,
   tabular, with a ≥24px hit target — but it is no longer a filled pill. Beside
   a filled ROLE pill it made the metadata line the loudest thing in the row,
   louder than the comment somebody wrote. The ground arrives on hover and
   focus, where a control should announce itself. */
/* …which was measured against readers and lost: the owner's own report is
   "don't know that little time is clickable". Ink-only affordance asks the
   reader to already know. So the control now carries a play glyph and a quiet
   border at rest — enough to read as a button without out-shouting the words
   beside it — and fills on hover/focus the way it always did. */
.ks-review-time {
  display: inline-flex;
  align-items: center;
  gap: .28rem;
  border: 1px solid color-mix(in oklab, var(--ks-accent-strong) 38%, transparent);
  cursor: pointer;
  font: inherit;
  font-size: var(--ks-body-sm);
  font-weight: 700;
  font-variant-numeric: tabular-nums;
  color: var(--ks-accent-strong);
  background: none;
  border-radius: var(--ks-r-pill);
  padding: .12rem .55rem .12rem .4rem;
  min-height: 26px;
  transition: background var(--duration-fast, .15s) var(--ease-out, ease-out),
    border-color var(--duration-fast, .15s) var(--ease-out, ease-out);
}
.ks-review-time-glyph { display: inline-flex; }

/* The row a notification delivered the reader to. A colour settle, not a
   movement, so it reads the same under reduced motion; the class is removed
   by the renderer once the moment has passed. */
.ks-review-item.is-located {
  background: var(--ks-accent-soft);
  border-radius: var(--ks-r-md);
  transition: background 1.2s var(--ease-out, ease-out);
}
.ks-review-time-glyph .ks-icon { width: .95em; height: .95em; }
.ks-review-time:hover,
.ks-review-time:focus-visible {
  background: var(--ks-accent-soft);
  border-color: var(--ks-accent-strong);
}

/* The role is metadata, not an award. Two filled accent pills per row — role
   and time — competed with the body and with each other; as a quiet uppercase
   label it still says who is speaking and stops out-ranking what they said.
   One treatment for every role now, so the non-staff override below is no
   longer needed: nobody wears the dojo's accent here. */
.ks-review-role {
  font-size: var(--ks-label);
  font-weight: 700;
  letter-spacing: .06em;
  text-transform: uppercase;
  color: var(--ks-ink-3);
  background: none;
  border-radius: 0;
  padding: 0;
}

/* The date is metadata about metadata — a step below the name and the role it
   sits beside. Kept at ink-3, so only the size moves. */
.ks-review-item > .ks-thread-who > .ks-thread-when { font-size: var(--ks-label); }

.ks-review-actions { display: flex; gap: 1rem; margin-top: .4rem; }
.ks-review-act {
  border: 0;
  background: none;
  cursor: pointer;
  font: inherit;
  font-size: var(--ks-body-sm);
  /* 500, not 700: these sat at the author's own weight and read as a second
     headline under every comment. The SIZE is deliberately unchanged — this
     file's own note records that .72rem actions ended up smaller than the
     metadata beside them — so the weight carries the whole reduction. Still
     always visible: hover is not a thing on half the devices holding this
     page. */
  font-weight: 500;
  color: var(--ks-ink-3);
  padding: .1rem 0;
  min-height: 24px;
}
.ks-review-act:hover { color: var(--ks-ink); }
.ks-review-act.is-danger:hover { color: var(--ks-danger, #b3261e); }

/* Replies nest one level under their parent — a rail, not a second card
   language. The list inherits .ks-thread's grid gap. */
/* ONE nesting signal, not three. A reply used to carry a rail AND a border AND
   a sunken fill — which in dark was very nearly black, so the answer to a
   comment looked like a hole cut in the card rather than something closer to
   the reader. The rail alone says "under this", and says it in both themes. */
.ks-review-replies {
  margin-top: .45rem;
  /* The reply carries a mark of its own, so it already indents by that column;
     the rail only has to say "under this" and can sit closer than it did. */
  padding-left: .75rem;
  border-left: 2px solid var(--ks-line-soft);
  gap: 0;
}
/* The rail hangs under the parent's TEXT, not under its mark — the second
   column the mark created is what a reply is a reply to. */
.ks-review-item > .ks-review-replies { grid-column: 2; }
.ks-review-replies > .ks-review-item {
  padding: .5rem 0 .45rem;
  border: 0;
  border-radius: 0;
  background: none;
}
.ks-review-replies > .ks-review-item + .ks-review-item {
  border-top: 1px solid var(--ks-line-soft);
}

.ks-review-orphan {
  margin: .35rem 0 0;
  font-size: var(--ks-body-sm);
  font-style: italic;
  color: var(--ks-ink-3);
}

.ks-review-empty {
  display: grid;
  gap: .2rem;
  padding: 1rem;
  border: 1px dashed var(--ks-line);
  border-radius: var(--ks-r-md);
  color: var(--ks-ink-3);
  font-size: var(--ks-body-sm);
}
.ks-review-empty strong { color: var(--ks-ink-2); }

/* ── the composer's anchor ────────────────────────────────────────────────
   The chip is the captured moment, held while the writer types. Its
   unavailable state is words, not a zero — the whole point is never to claim
   a position that was not read. */
.ks-review-anchor-row { display: flex; align-items: center; gap: .6rem; }
.ks-review-anchor {
  font-size: var(--ks-body-sm);
  font-weight: 800;
  font-variant-numeric: tabular-nums;
  color: var(--ks-accent-strong);
  background: var(--ks-accent-soft);
  border-radius: var(--ks-r-pill);
  padding: .15rem .55rem;
}
.ks-review-anchor.is-unavailable {
  color: var(--ks-ink-3);
  background: var(--ks-surface-sunken);
  font-weight: 600;
}
.ks-review-composer .ks-review-input { flex: 1; }
/* Narrower and a touch shorter than the shared .ks-btn, which is sized for a
   page's primary action and towered beside a two-line field. Still filled,
   still the primary — the hierarchy is unchanged, only the area. min-height
   holds the target that the reduced padding would otherwise give up. */
.ks-review-composer .ks-review-send {
  margin-left: auto;
  padding: .7rem 1.15rem;
  min-height: 40px;
}

/* ── the composer, finished ───────────────────────────────────────────────
   Scoped to .ks-review-composer throughout: .ks-textarea and .ks-btn are
   shared components and this pass has no business repainting them anywhere
   else.

   What was wrong. The box and the button were two objects of different heights
   sitting side by side with a hard border between them and the card, and the
   anchor chip floated above the pair with no relationship to either. Now the
   three read as one control: the anchor sits on the composer's own line, the
   field is a quiet inset rather than an outlined box, and the button aligns to
   the field's baseline edge instead of towering beside it. Sizes are unchanged
   — the button keeps its own component's padding and hit area. */
.ks-review-composer { margin-top: 0; gap: .3rem; }
.ks-review-composer .ks-composer-row { align-items: flex-end; gap: .55rem; }
.ks-review-composer .ks-review-input {
  border-color: var(--ks-line-soft);
  background: var(--ks-surface-sunken);
  border-radius: var(--ks-r-md);
  padding: .55rem .7rem;
  line-height: 1.45;
}
/* The field earns its edge when it is being used, rather than shouting for
   attention while it is empty. The focus ring is untouched — see the
   :focus-visible rule below, which still draws it. */
.ks-review-composer .ks-review-input:hover { border-color: var(--ks-line); }
/* Restated, not decoration: the quiet resting border above has the same
   specificity as .ks-textarea:focus-visible and sits later in the file, so
   without this the field kept its resting edge while focused and lost the
   accent the rest of the app draws. The outline ring was never at risk. */
.ks-review-composer .ks-review-input:focus-visible { border-color: var(--ks-accent); }

/* Dark inverts what "inset" means. A sunken fill in dark is #04080F against a
   #0A1324 card — a hole, the same mistake the nested reply used to make, and a
   field a writer is invited to type into should not read as the darkest thing
   on the surface. Raised instead: it sits a step ABOVE the card, which is what
   an affordance does in a dark theme. Contrast is unharmed — placeholder ink
   clears 5.1:1 there and typed text 12.4:1. */
:root[data-theme="dark"] .ks-review-composer .ks-review-input {
  background: var(--ks-surface-raised);
}
@media (prefers-color-scheme: dark) {
  :root:not([data-theme]) .ks-review-composer .ks-review-input {
    background: var(--ks-surface-raised);
  }
}

/* The captured moment reads as a label on the field it belongs to, not as a
   second accent pill competing with the row timestamps below it. "No video
   position" is deliberately the quietest of the two states — it reports an
   absence, and an absence should not be the loudest thing on the surface. */
.ks-review-anchor-row { gap: .45rem; min-height: 20px; }
.ks-review-composer .ks-review-anchor {
  font-weight: 700;
  padding: .1rem .4rem;
  border-radius: var(--ks-r-xs);
}
/* A captured moment is worth reading; its ABSENCE is a footnote. Dropped to
   the label size so "No video position" stops introducing the composer and
   merely qualifies it — the state that reports nothing should weigh least. */
.ks-review-composer .ks-review-anchor.is-unavailable {
  background: none;
  padding-left: 0;
  color: var(--ks-ink-3);
  font-size: var(--ks-label);
  font-weight: 600;
}

/* Focus, on surfaces that are not inside .ks-app — the Match Library mounts
   this outside that scope, and a control without a visible focus ring is
   unusable from a keyboard. Same ring as the app surface draws. */
.ks-review-root :is(button, a, [tabindex]):focus-visible,
.ks-review-composer :is(button, textarea):focus-visible {
  outline: 2px solid var(--ks-focus-ring);
  outline-offset: 2px;
  border-radius: var(--ks-r-control-sm);
}

@media (pointer: coarse) {
  .ks-review-act, .ks-review-time { min-height: 32px; padding-top: .25rem; padding-bottom: .25rem; }
}


/* ══════════════════════════════════════════════════════════════════════
   ks-action-menu.v1.css
   ══════════════════════════════════════════════════════════════════════ */
    /* ── Action menu ─────────────────────────────────────────────────────────
       The overflow home for management actions. Token-derived throughout, so
       every tenant's theme owns it. */
    .ks-action-menu {
      position: relative;
      display: inline-flex;
    }

    .ks-action-menu-btn .ks-icon {
      width: 12px;
      height: 12px;
      margin-left: .3rem;
      transition: transform var(--duration-fast) var(--ease-out);
    }

    .ks-action-menu.is-open .ks-action-menu-btn .ks-icon {
      transform: rotate(180deg);
    }

    .ks-action-menu-list {
      position: absolute;
      top: calc(100% + 6px);
      right: 0;
      z-index: 30;
      min-width: 170px;
      padding: .35rem;
      background: var(--ks-surface);
      border: 1px solid var(--ks-line);
      border-radius: var(--radius-md);
      box-shadow: var(--ks-shadow-2);
      display: grid;
      gap: 2px;
    }

    /* display:grid above would silently defeat the hidden attribute. */
    .ks-action-menu-list[hidden] {
      display: none;
    }

    .ks-action-menu-item {
      display: flex;
      align-items: center;
      gap: .55rem;
      width: 100%;
      padding: .55rem .7rem;
      border: 0;
      border-radius: var(--radius-sm);
      background: none;
      color: var(--ks-ink);
      font-size: .82rem;
      font-weight: 700;
      text-align: left;
      cursor: pointer;
    }

    .ks-action-menu-item .ks-icon {
      width: 15px;
      height: 15px;
      flex-shrink: 0;
      color: var(--ks-ink-3);
    }

    .ks-action-menu-item:hover,
    .ks-action-menu-item:focus-visible {
      background: var(--ks-surface-sunken);
    }

    .ks-action-menu-item:focus-visible {
      outline: 2px solid var(--ks-focus-ring);
      outline-offset: -2px;
    }

    .ks-action-menu-item.is-danger {
      color: var(--ks-danger);
    }

    .ks-action-menu-item.is-danger .ks-icon {
      color: var(--ks-danger);
    }

    @media (prefers-reduced-motion: reduce) {
      .ks-action-menu-btn .ks-icon {
        transition: none;
      }
    }


/* ══════════════════════════════════════════════════════════════════════
   ks-coach-surface.v1.css
   ══════════════════════════════════════════════════════════════════════ */
    /* ── The coach's work surfaces on the roster ─────────────────────────────
       Needs-your-attention rows and the other-dojos universe. Token-derived
       throughout. The universe renders nothing when it has nothing true to
       say; the attention queue tells its own readers when it is CLEAR, because
       for someone on duty "no work" and "no access" must not look alike. */
    .ks-coach-attention,
    .ks-coach-universe {
      max-width: var(--ks-measure);
      margin: 0 auto 1.4rem;
    }

    .ks-coach-universe {
      margin-top: 1.8rem;
    }

    .ks-attn-title,
    .ks-universe-title {
      margin: 0 0 .7rem;
      font-size: .95rem;
      font-weight: 800;
      color: var(--ks-ink);
    }

    .ks-attn-list,
    .ks-universe-list {
      display: grid;
      gap: .4rem;
    }

    .ks-attn-row,
    .ks-universe-row {
      display: flex;
      align-items: center;
      gap: .8rem;
      width: 100%;
      padding: .65rem .85rem;
      background: var(--ks-surface);
      border: 1px solid var(--ks-line);
      border-radius: var(--radius-md);
      text-align: left;
      cursor: pointer;
      text-decoration: none;
      color: var(--ks-ink);
      transition: border-color var(--duration-fast) var(--ease-out), background var(--duration-fast) var(--ease-out);
    }

    .ks-attn-row:hover,
    .ks-attn-row:focus-visible,
    .ks-universe-row:hover,
    .ks-universe-row:focus-visible {
      border-color: var(--ks-accent);
      background: var(--ks-accent-soft);
    }

    .ks-attn-row:focus-visible,
    .ks-universe-row:focus-visible {
      outline: 2px solid var(--ks-focus-ring);
      outline-offset: 1px;
    }

    .ks-attn-who,
    .ks-universe-name {
      font-weight: 800;
      font-size: .88rem;
      white-space: nowrap;
    }

    .ks-attn-what {
      flex: 1;
      min-width: 0;
      font-size: .82rem;
      color: var(--ks-ink-2);
      overflow: hidden;
      text-overflow: ellipsis;
      white-space: nowrap;
    }

    .ks-attn-chip {
      flex-shrink: 0;
      padding: .2rem .6rem;
      border-radius: var(--radius-full);
      font-size: .68rem;
      font-weight: 800;
      letter-spacing: .04em;
    }

    .ks-attn-chip.is-review {
      background: var(--ks-accent-soft);
      color: var(--ks-accent-strong);
    }

    .ks-attn-chip.is-reply {
      background: var(--ks-surface-sunken);
      color: var(--ks-ink);
      border: 1px solid var(--ks-line);
    }

    .ks-attn-more {
      margin: .5rem 0 0;
      font-size: .78rem;
      color: var(--ks-ink-3);
    }

    .ks-universe-belt {
      font-size: .78rem;
      color: var(--ks-ink-2);
    }

    .ks-universe-dojo {
      margin-left: auto;
      flex-shrink: 0;
      font-size: .72rem;
      font-weight: 800;
      letter-spacing: .05em;
      text-transform: uppercase;
      color: var(--ks-ink-3);
    }

    @media (max-width: 640px) {
      .ks-attn-row {
        flex-wrap: wrap;
        gap: .3rem .8rem;
      }

      .ks-attn-what {
        flex-basis: 100%;
        order: 3;
      }
    }

    .ks-universe-none {
      margin: 0;
      font-size: .82rem;
      color: var(--ks-ink-3);
    }

    .ks-attn-clear {
      margin: 0;
      padding: .65rem .85rem;
      font-size: .82rem;
      color: var(--ks-ink-3);
      background: var(--ks-surface);
      border: 1px dashed var(--ks-line);
      border-radius: var(--radius-md);
    }


/* ══════════════════════════════════════════════════════════════════════
   ks-showroom.v1.css
   ══════════════════════════════════════════════════════════════════════ */
/* ── The product tour (demo tenants only) ─────────────────────────────────────
 * Styling for view-showroom and the home invitation. Composition only: the
 * roster cards, fight items, timeline, hub card and coach queue inside the
 * panels keep their own component styles — this file lays them out and adds
 * the tour's own furniture (intro, sample-data chips, tool grid).
 *
 * The .sr-vr block is the tour's skin for the shared video-review enhancement
 * (ks-video-review.v1.js). The component's structure and behaviour are shared
 * with the marketing homepage; its colours there come from that page's own
 * token system, which tenant pages do not load — so the skin is restated here
 * against tenant tokens, scoped under .sr-vr, and the homepage keeps its own.
 * The classifier files this whole sheet into bundle 6 (utilities), which loads
 * last — the right cascade position for a surface that arranges components.
 * ─────────────────────────────────────────────────────────────────────────── */

/* ── Home invitation ── */
/* THE HIDE MUST LIVE HERE. Every ks-feature-hidden rule in the platform is
   li-scoped — it is a NAV-ITEM contract (dojo-global-feature-nav-hide.v1.css),
   and a section wearing the class alone is hidden by nothing. That gap put
   this invitation on a real customer tenant's homepage: the presence pass set
   the class, every probe read the class, and the section rendered anyway.
   Belt and braces with the hidden attribute the markup now ships. */
.sr-invite.ks-feature-hidden,
.sr-invite[hidden] {
  display: none;
}

.sr-invite {
  margin: 1.6rem auto 0;
  max-width: 1120px;
  padding: 0 1.25rem;
}

.sr-invite-inner {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 1.1rem 2rem;
  padding: 1.35rem 1.5rem;
  border-radius: var(--ks-r-lg);
  background: linear-gradient(135deg, var(--ks-navy) 0%, var(--ks-navy-2) 100%);
  color: var(--ks-white);
  box-shadow: var(--ks-e2);
}

.sr-invite-copy { flex: 1 1 26rem; min-width: 0; }

.sr-invite-kicker {
  display: inline-block;
  font-size: .68rem;
  font-weight: 800;
  letter-spacing: .12em;
  text-transform: uppercase;
  /* The tenant's accent on the navy panel. The fallback is a lightened blue
     rather than the brand blue: at kicker size on this ground the brand step
     reads under 4.5:1, and a tenant with no accent still deserves legibility. */
  color: var(--tn-accent, #9DB9FF);
}

.sr-invite-title {
  margin: .3rem 0 0;
  font-size: 1.35rem;
  line-height: 1.2;
}

.sr-invite-lede {
  margin: .45rem 0 0;
  max-width: 46rem;
  font-size: .92rem;
  line-height: 1.55;
  color: rgba(255, 255, 255, .82);
}

.sr-invite-actions {
  display: flex;
  flex-wrap: wrap;
  gap: .6rem;
}

/* The ghost buttons sit on the navy panel here, not on the band. */
.sr-invite .ks-btn-ghost {
  color: var(--ks-white);
  border-color: rgba(255, 255, 255, .35);
}

.sr-invite .ks-btn-ghost:hover { border-color: rgba(255, 255, 255, .7); }

/* ── The tour's shell ── */
/* The view owns its canvas, the way #view-team-preview and #view-admin do.
   Without this the page falls through to the dark shell underneath and every
   token below — which resolve light — puts dark ink on a dark ground. */
#view-showroom {
  background: var(--ks-canvas);
  color: var(--ks-ink);
}

.sr-shell {
  max-width: 1120px;
  margin: 0 auto;
  padding: 1.4rem 1.25rem 3.5rem;
}

.sr-notice {
  margin: 0 0 1.2rem;
  padding: .7rem 1rem;
  border-radius: var(--ks-r-sm);
  background: var(--ks-warm-2);
  border: 1px solid var(--ks-warm-line);
  font-size: .85rem;
  line-height: 1.5;
  color: var(--ks-n-700);
}

.sr-tabs .ks-tablist {
  display: flex;
  flex-wrap: wrap;
  gap: .4rem;
  margin-bottom: 1.4rem;
}

.sr-panel { outline: none; }

.sr-intro {
  margin: 0 0 1.6rem;
  max-width: 52rem;
  font-size: 1.02rem;
  line-height: 1.6;
  color: var(--ks-n-700);
}

/* ── Blocks ── */
.sr-block { margin: 0 0 2.4rem; }

.sr-block-head {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: .3rem .6rem;
  margin-bottom: 1rem;
}

.sr-block-title {
  margin: 0;
  font-size: 1.15rem;
  line-height: 1.25;
}

.sr-block-lede {
  flex-basis: 100%;
  margin: .2rem 0 0;
  max-width: 52rem;
  font-size: .88rem;
  line-height: 1.55;
  color: var(--ks-ink-2);
}

.sr-chip {
  display: inline-block;
  padding: .14rem .55rem;
  border-radius: var(--ks-r-pill);
  background: var(--ks-n-100);
  border: 1px solid var(--ks-n-200);
  font-size: .64rem;
  font-weight: 800;
  letter-spacing: .08em;
  text-transform: uppercase;
  /* --ks-ink-2, not the --ks-n-500 primitive this read before. Two reasons and
     both are stated by the token file: a component that reads a primitive
     directly cannot be themed, and n-500 on n-100 measured 4.24:1 -- under 4.5
     for text this small, on the surface a prospect is shown. */
  color: var(--ks-ink-2);
  white-space: nowrap;
}

.sr-caption {
  margin: .8rem 0 0;
  font-size: .82rem;
  line-height: 1.55;
  color: var(--ks-ink-2);
}

.sr-prose p {
  margin: 0 0 .8rem;
  max-width: 56rem;
  font-size: .92rem;
  line-height: 1.6;
  color: var(--ks-n-700);
}

.sr-prose p:last-child { margin-bottom: 0; }

/* ── Athlete panel ── */
.sr-athlete-head {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: .9rem 1.4rem;
  padding: 1rem 1.2rem;
  margin-bottom: 1.1rem;
  border-radius: var(--ks-r-md);
  background: var(--ks-white);
  box-shadow: var(--ks-e1);
}

.sr-athlete-name {
  display: block;
  font-size: 1.2rem;
  font-weight: 800;
}

.sr-athlete-meta {
  display: block;
  margin-top: .15rem;
  font-size: .82rem;
  color: var(--ks-n-500);
}

.sr-athlete-stats {
  display: flex;
  flex-wrap: wrap;
  gap: .5rem;
}

.sr-stat {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: .1rem;
  min-width: 6.2rem;
  padding: .5rem .8rem;
  border-radius: var(--ks-r-sm);
  background: var(--ks-n-50);
  border: 1px solid var(--ks-n-100);
  font-size: .66rem;
  font-weight: 700;
  letter-spacing: .06em;
  text-transform: uppercase;
  color: var(--ks-n-500);
}

.sr-stat strong {
  font-size: 1.05rem;
  letter-spacing: 0;
  color: var(--ks-n-900);
}

/* ── Coach panel ── */
.sr-medals {
  display: flex;
  flex-wrap: wrap;
  gap: .5rem;
  margin-bottom: .9rem;
}

.sr-medal {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: .1rem;
  min-width: 5.4rem;
  padding: .5rem .8rem;
  border-radius: var(--ks-r-sm);
  background: var(--ks-white);
  box-shadow: var(--ks-e1);
  font-size: .66rem;
  font-weight: 700;
  letter-spacing: .06em;
  text-transform: uppercase;
  color: var(--ks-n-500);
}

.sr-medal strong { font-size: 1.05rem; color: var(--ks-n-900); }

.sr-medal-gold strong { color: #9A7B1E; }
.sr-medal-silver strong { color: var(--ks-n-500); }
.sr-medal-bronze strong { color: #8A5A2B; }

.sr-placements {
  margin: 0;
  padding: 0;
  list-style: none;
}

.sr-placements li {
  padding: .5rem 0;
  border-bottom: 1px solid var(--ks-n-100);
  font-size: .88rem;
  line-height: 1.5;
  color: var(--ks-n-700);
}

.sr-placements li:last-child { border-bottom: 0; }

/* ── Admin panel ── */
.sr-live-grid,
.sr-tool-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(15rem, 1fr));
  gap: .8rem;
}

.sr-live-card {
  display: flex;
  flex-direction: column;
  gap: .35rem;
  padding: 1rem 1.1rem;
  border-radius: var(--ks-r-md);
  background: var(--ks-white);
  box-shadow: var(--ks-e1);
}

.sr-live-card h3 { margin: 0; font-size: .95rem; }

.sr-live-card p {
  margin: 0;
  flex: 1;
  font-size: .84rem;
  line-height: 1.5;
  color: var(--ks-n-500);
}

.sr-live-link {
  align-self: flex-start;
  margin-top: .3rem;
  padding: 0;
  border: 0;
  background: none;
  font: inherit;
  font-size: .82rem;
  font-weight: 700;
  color: var(--tn-accent-ink, var(--ks-blue-deep));
  cursor: pointer;
}

.sr-live-link:hover { text-decoration: underline; }

.sr-tool-group {
  margin: 1.4rem 0 .6rem;
  font-size: .72rem;
  font-weight: 800;
  letter-spacing: .1em;
  text-transform: uppercase;
  /* n-500, not n-400: at this size the lighter step measures under 3:1 on the
     page ground, and a group label is information, not decoration. */
  color: var(--ks-n-500);
}

.sr-tool-group:first-of-type { margin-top: 0; }

.sr-tool-card {
  display: flex;
  flex-direction: column;
  gap: .25rem;
  padding: .85rem 1rem;
  border-radius: var(--ks-r-sm);
  background: var(--ks-n-50);
  border: 1px solid var(--ks-n-100);
}

.sr-tool-card strong { font-size: .88rem; }

.sr-tool-card span {
  font-size: .8rem;
  line-height: 1.45;
  color: var(--ks-n-500);
}

/* ── The video-review skin, tenant tokens ─────────────────────────────────
 * Structure and behaviour are ks-video-review.v1.js's; only colour and
 * chrome are restated here, scoped so nothing leaks beyond the tour. */
.sr-vr {
  max-width: 620px;
  background: var(--ks-white);
  border-radius: var(--ks-r-md);
  overflow: hidden;
  box-shadow: var(--ks-e2);
}

.sr-vr .vr-bar {
  display: flex;
  align-items: center;
  gap: .6rem;
  padding: .6rem .95rem;
  background: var(--ks-navy);
}

.sr-vr .vr-bar b {
  font-size: .7rem;
  font-weight: 800;
  letter-spacing: .12em;
  color: var(--ks-white);
}

.sr-vr .vr-bar em {
  font-style: normal;
  margin-left: auto;
  font-size: .6rem;
  font-weight: 700;
  letter-spacing: .04em;
  color: rgba(255, 255, 255, .62);
}

.sr-vr .vr-frame { position: relative; overflow: hidden; background: #08060a; }

.sr-vr .vr-frame img {
  display: block;
  width: 100%;
  height: auto;
  aspect-ratio: 16 / 9;
  object-fit: cover;
  transition: object-position .45s ease, transform .45s ease;
}

.sr-vr .vr-frame.vr-f2 img { object-position: 34% 16%; transform: scale(1.34); }
.sr-vr .vr-frame.vr-f3 img { object-position: 70% 26%; transform: scale(1.16); }

.sr-vr .vr-frame::after {
  content: "";
  position: absolute;
  inset: auto 0 0 0;
  height: 34%;
  pointer-events: none;
  background: linear-gradient(to top, rgba(0, 0, 0, .78), transparent);
}

.sr-vr .vr-chrome {
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  z-index: 2;
  padding: 0 .95rem .7rem;
}

.sr-vr .vr-track { position: relative; height: 22px; }

.sr-vr .vr-rail {
  position: absolute;
  left: 0;
  right: 0;
  top: 9px;
  height: 4px;
  border-radius: 2px;
  background: rgba(255, 255, 255, .28);
}

.sr-vr .vr-played {
  position: absolute;
  left: 0;
  top: 9px;
  height: 4px;
  border-radius: 2px;
  background: var(--tn-accent, var(--ks-blue-bright));
  transition: width .35s ease;
}

.sr-vr .vr-playhead {
  position: absolute;
  top: 11px;
  width: 13px;
  height: 13px;
  margin: -6.5px 0 0 -6.5px;
  border-radius: 50%;
  background: var(--ks-white);
  box-shadow: 0 0 0 2px rgba(0, 0, 0, .25);
  transition: left .35s ease;
}

.sr-vr .vr-marker {
  position: absolute;
  top: 2px;
  transform: translateX(-50%);
  width: 18px;
  height: 18px;
  display: grid;
  place-items: center;
  padding: 0;
  border: 0;
  background: none;
  cursor: pointer;
}

.sr-vr .vr-marker i {
  width: 9px;
  height: 9px;
  border-radius: 50%;
  background: var(--tn-accent, var(--ks-blue-bright));
  border: 2px solid rgba(0, 0, 0, .3);
  transition: transform .2s ease, background .2s ease;
}

.sr-vr .vr-marker.is-on i {
  transform: scale(1.35);
  background: var(--ks-white);
  border-color: var(--tn-accent, var(--ks-blue-bright));
}

.sr-vr .vr-marker:focus-visible {
  outline: 2px solid var(--ks-white);
  outline-offset: 2px;
  border-radius: 50%;
}

.sr-vr .vr-ctl { display: flex; align-items: center; gap: .7rem; margin-top: .1rem; }
.sr-vr .vr-ctl svg { display: block; width: 15px; height: 15px; }
.sr-vr .vr-ctl .sp { margin-left: auto; }

.sr-vr .vr-clock {
  font-variant-numeric: tabular-nums;
  font-size: .78rem;
  font-weight: 600;
  color: rgba(255, 255, 255, .95);
}

.sr-vr .vr-thread { padding: .95rem 1.1rem 1rem; }

.sr-vr .vr-head { display: flex; align-items: center; gap: .55rem; }

.sr-vr .vr-stamp {
  font-variant-numeric: tabular-nums;
  font-weight: 800;
  font-size: .8rem;
  color: var(--ks-white);
  background: var(--ks-navy);
  padding: .1rem .45rem;
  border-radius: var(--ks-r-xs);
}

.sr-vr .vr-who {
  font-size: .68rem;
  font-weight: 800;
  letter-spacing: .09em;
  text-transform: uppercase;
  /* Was --ks-n-400 at 2.88:1 -- the widest gap on the tour, on a label that
     names who is speaking in a coaching thread. Scoped to .sr-vr, so the shared
     .vr-who on My Dashboard and the Match Library keeps its own treatment and
     is judged by its own specs. */
  color: var(--ks-ink-2);
}

.sr-vr .vr-note {
  margin: .4rem 0 0;
  font-size: .95rem;
  line-height: 1.55;
  color: var(--ks-n-900);
}

.sr-vr .vr-replywrap {
  margin: .6rem 0 0 .9rem;
  padding-left: .8rem;
  border-left: 2px solid var(--ks-n-200);
}

.sr-vr .vr-reply {
  margin: .15rem 0 0;
  font-size: .9rem;
  line-height: 1.5;
  color: var(--ks-n-700);
}

.sr-vr .vr-sent {
  display: flex;
  align-items: center;
  gap: .5rem;
  margin-top: .75rem;
  padding-top: .65rem;
  border-top: 1px solid var(--ks-n-100);
  font-size: .78rem;
  color: var(--ks-n-500);
}

.sr-vr .vr-sent i {
  width: 7px;
  height: 7px;
  border-radius: 50%;
  background: var(--tn-accent, var(--ks-blue));
}

.sr-vr .vr-foot {
  display: flex;
  align-items: center;
  gap: .6rem;
  padding: .6rem .95rem;
  background: var(--ks-n-50);
  border-top: 1px solid var(--ks-n-100);
}

.sr-vr .vr-play {
  font: inherit;
  font-size: .8rem;
  font-weight: 700;
  color: var(--ks-navy);
  background: var(--ks-white);
  border: 1px solid var(--ks-n-300);
  border-radius: var(--ks-r-pill);
  padding: .35rem .9rem;
  cursor: pointer;
}

.sr-vr .vr-play:hover { border-color: var(--ks-n-400); }

.sr-vr .vr-hint { font-size: .8rem; color: var(--ks-n-500); }

/* Visually hidden, for the review's live region. */
.sr-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 hub card inside the tour keeps its own width discipline ──
   Its colour comes from the hub's own token scope: .sr-hub joins the
   #view-tournament-hub custom-property block in dojo-homann-team-hub.v1.css,
   so the card here is the card there. */
.sr-hub { max-width: 680px; }

/* ── Match cards on the tour's light canvas ───────────────────────────────
   .fight-item is a dark-canvas component: its own stylesheet gives it white
   ink over a translucent dark card, which vanishes on the tour's light
   ground. This is a surface skin, not a redesign — same layout, ink that
   reads here. Scoped to the view so the Match Library keeps its dark self. */
#view-showroom .fight-item {
  background: var(--ks-white);
  border: 1px solid var(--ks-n-100);
  box-shadow: var(--ks-e1);
}

#view-showroom .fi-title { color: var(--ks-n-900); }
#view-showroom .fi-meta { color: var(--ks-n-500); }
#view-showroom .fi-score { color: var(--ks-n-900); }
#view-showroom .fi-arr { color: var(--ks-n-400); }

/* The tour's match cards all carry the platform's own match-review still
   (buildFightItem's thumbnailUrl seam), reframed per card — the review
   strip's technique: one photograph, several framings, sequential
   storytelling without pretending unrelated images are different matches. */
#view-showroom .fi-thumb { overflow: hidden; }
#view-showroom .fi-thumb img { object-fit: cover; }
#view-showroom .fight-item:nth-child(3n+1) .fi-thumb img { object-position: 30% 18%; transform: scale(1.28); }
#view-showroom .fight-item:nth-child(3n+2) .fi-thumb img { object-position: 66% 24%; transform: scale(1.42); }
#view-showroom .fight-item:nth-child(3n) .fi-thumb img { object-position: 50% 34%; transform: scale(1.15); }

/* ── The console demonstration ────────────────────────────────────────────
   The approvals table mirrors the admin shell's own table voice
   (dojo-admin-dashboard-shell.v1.css #view-admin .admin-table) in the tour's
   token vocabulary — the --adm-* tokens live on #view-admin and do not reach
   here. The ta-* row classes are the admin dashboard's own and style
   themselves. */
.sr-admin-card {
  overflow-x: auto;
  -webkit-overflow-scrolling: touch;
  border-radius: var(--ks-r-md);
  background: var(--ks-white);
  box-shadow: var(--ks-e1);
}

.sr-admin-table { border-collapse: separate; border-spacing: 0; width: 100%; }

.sr-admin-table th {
  padding: 11px 14px;
  text-align: left;
  background: var(--ks-n-50);
  color: var(--ks-n-500);
  font-weight: 700;
  text-transform: uppercase;
  font-size: .6rem;
  letter-spacing: .12em;
  border-bottom: 1px solid var(--ks-n-200);
}

.sr-admin-table td {
  padding: 13px 14px;
  border-bottom: 1px solid var(--ks-n-100);
  color: var(--ks-n-900);
  vertical-align: middle;
  font-size: .85rem;
}

.sr-admin-table tbody tr:last-child td { border-bottom: none; }

.sr-admin-relation { display: block; margin-top: .15rem; }

.sr-brand-row {
  display: flex;
  flex-wrap: wrap;
  gap: .6rem 1.2rem;
  padding: 1rem 1.2rem;
  border-radius: var(--ks-r-md);
  background: var(--ks-white);
  box-shadow: var(--ks-e1);
  font-size: .82rem;
  font-weight: 700;
  color: var(--ks-n-700);
}

.sr-brand-item { display: inline-flex; align-items: center; gap: .45rem; }

/* The condensed inventory: one row per group, names only. */
.sr-catalog {
  display: flex;
  flex-direction: column;
  border-radius: var(--ks-r-md);
  background: var(--ks-white);
  box-shadow: var(--ks-e1);
  overflow: hidden;
}

.sr-catalog-row {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: .2rem 1rem;
  padding: .75rem 1.2rem;
  border-bottom: 1px solid var(--ks-n-100);
}

.sr-catalog-row:last-child { border-bottom: none; }

.sr-catalog-row strong {
  flex: 0 0 10.5rem;
  font-size: .7rem;
  font-weight: 800;
  letter-spacing: .1em;
  text-transform: uppercase;
  color: var(--ks-n-500);
}

.sr-catalog-row span {
  flex: 1 1 16rem;
  font-size: .86rem;
  line-height: 1.6;
  color: var(--ks-n-700);
}

/* ── Small screens ── */
@media (max-width: 700px) {
  .sr-invite { padding: 0 1rem; }
  .sr-invite-inner { padding: 1.1rem 1.1rem 1.2rem; }
  .sr-invite-actions { width: 100%; }
  .sr-invite-actions .ks-btn { flex: 1 1 auto; }
  .sr-shell { padding: 1.1rem 1rem 3rem; }
  .sr-athlete-stats { width: 100%; }
  .sr-stat { flex: 1 1 auto; }
}

@media (prefers-reduced-motion: reduce) {
  .sr-vr .vr-frame img,
  .sr-vr .vr-played,
  .sr-vr .vr-playhead,
  .sr-vr .vr-marker i { transition: none; }
}

/* ── THE OPERATIONS HALF OF THE TOUR ─────────────────────────────────────────
   Progression, the register, what the dojo sells, and the Sensei. Added
   2026-08-31: the tour showed the mid-August product, so the tab a prospect
   opened to see member-only screens had no classes, register, curriculum,
   memberships or Sensei on it.

   Built from the same surface, radius and elevation tokens as .sr-admin-card
   above, so these read as more of the same console rather than four new
   widgets. Nothing here introduces a colour: state is carried by the status
   tokens the platform already defines. */

/* ── Progression ─────────────────────────────────────────────────────────── */
.sr-prog {
  background: var(--ks-white);
  border-radius: var(--ks-r-md);
  box-shadow: var(--ks-e1);
  padding: 1.15rem 1.25rem 1rem;
}
.sr-prog-head {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: .5rem .75rem;
  padding-bottom: .8rem;
  border-bottom: 1px solid var(--ks-line);
}
.sr-prog-head b { font-size: 1.02rem; }
.sr-prog-rung {
  font-size: .78rem;
  font-weight: 700;
  letter-spacing: .04em;
  text-transform: uppercase;
  color: var(--ks-accent-strong);
}

/* "96 of 120" is the sentence a parent actually asks, so the number leads and
   the bar is the supporting evidence rather than the other way round. */
.sr-prog-metric {
  display: grid;
  grid-template-columns: 1fr auto;
  align-items: baseline;
  gap: .2rem .75rem;
  margin-top: .85rem;
}
.sr-prog-metric-label { font-size: .84rem; color: var(--ks-ink-2); }
.sr-prog-metric b { font-size: .9rem; font-variant-numeric: tabular-nums; }
.sr-prog-bar {
  grid-column: 1 / -1;
  height: 6px;
  border-radius: 999px;
  background: var(--ks-surface-sunken);
  overflow: hidden;
}
.sr-prog-bar i {
  display: block;
  height: 100%;
  border-radius: inherit;
  background: var(--ks-accent);
}

.sr-prog-reqs { list-style: none; margin: 1.1rem 0 0; padding: 0; }
.sr-prog-reqs li {
  display: grid;
  grid-template-columns: auto 1fr;
  align-items: baseline;
  gap: .2rem .7rem;
  padding: .5rem 0;
  border-top: 1px solid var(--ks-line);
}
/* A WORD, NOT ONLY A COLOUR. "Signed off" and "To do" both read in grey-scale
   and to a colour-blind reader; the tint is a second signal, never the only
   one. Same reasoning the button system uses for danger. */
.sr-prog-mark {
  grid-row: span 2;
  align-self: center;
  font-size: .64rem;
  font-weight: 800;
  letter-spacing: .06em;
  text-transform: uppercase;
  padding: .25rem .5rem;
  border-radius: var(--ks-r-control-sm);
  white-space: nowrap;
  background: var(--ks-surface-sunken);
  color: var(--ks-ink-3);
}
.sr-prog-reqs li.is-done .sr-prog-mark {
  background: color-mix(in oklab, var(--ks-ok) 12%, var(--ks-white));
  color: var(--ks-ok);
}
.sr-prog-req { font-size: .9rem; }
.sr-prog-reqs li em {
  grid-column: 2;
  font-style: normal;
  font-size: .72rem;
  letter-spacing: .04em;
  text-transform: uppercase;
  color: var(--ks-ink-3);
}

/* ── The register ────────────────────────────────────────────────────────── */
.sr-reg {
  background: var(--ks-white);
  border-radius: var(--ks-r-md);
  box-shadow: var(--ks-e1);
  overflow: hidden;
}
.sr-reg-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: .75rem;
  padding: .9rem 1.15rem;
  border-bottom: 1px solid var(--ks-line);
  background: var(--ks-surface-sunken);
}
.sr-reg-head em {
  font-style: normal;
  font-size: .72rem;
  letter-spacing: .05em;
  text-transform: uppercase;
  color: var(--ks-ink-3);
}
.sr-reg-list { list-style: none; margin: 0; padding: 0; }
.sr-reg-list li {
  display: grid;
  grid-template-columns: 1fr auto;
  align-items: baseline;
  gap: .15rem .75rem;
  padding: .62rem 1.15rem;
  border-top: 1px solid var(--ks-line);
  font-size: .9rem;
}
.sr-reg-list li:first-child { border-top: 0; }
.sr-reg-list li b {
  font-size: .68rem;
  font-weight: 800;
  letter-spacing: .06em;
  text-transform: uppercase;
}
.sr-reg-list li.is-present b { color: var(--ks-ok); }
.sr-reg-list li.is-excused b { color: var(--ks-warn); }
.sr-reg-list li.is-absent b { color: var(--ks-ink-3); }
.sr-reg-list li em {
  grid-column: 1 / -1;
  font-style: normal;
  font-size: .78rem;
  color: var(--ks-ink-3);
}
/* The signal a dojo owner buys this for, so it is the one thing here that gets
   a tinted band rather than another row. */
.sr-reg-drift {
  margin: 0;
  padding: .85rem 1.15rem;
  border-top: 1px solid var(--ks-line);
  background: color-mix(in oklab, var(--ks-warn) 8%, var(--ks-white));
  font-size: .84rem;
  line-height: 1.5;
  color: var(--ks-ink-2);
}

/* ── Plans ───────────────────────────────────────────────────────────────── */
.sr-plans {
  display: grid;
  /* minmax(0,1fr), never bare 1fr: a bare 1fr is minmax(AUTO,1fr) and a long
     plan name would push the track wider than its share. The house rule from
     the mobile text-autosizing blowout. */
  grid-template-columns: repeat(auto-fit, minmax(min(14rem, 100%), 1fr));
  gap: .8rem;
}
.sr-plan {
  display: grid;
  align-content: start;
  gap: .2rem;
  background: var(--ks-white);
  border-radius: var(--ks-r-md);
  box-shadow: var(--ks-e1);
  padding: .95rem 1.05rem;
}
.sr-plan b { font-size: .95rem; }
.sr-plan-price {
  font-size: 1.15rem;
  font-weight: 800;
  letter-spacing: -.02em;
  font-variant-numeric: tabular-nums;
}
.sr-plan em { font-style: normal; font-size: .82rem; color: var(--ks-ink-2); }
.sr-plan-members {
  margin-top: .35rem;
  font-size: .72rem;
  letter-spacing: .05em;
  text-transform: uppercase;
  color: var(--ks-ink-3);
}

/* ── The Sensei exchange ─────────────────────────────────────────────────── */
.sr-ask {
  background: var(--ks-white);
  border-radius: var(--ks-r-md);
  box-shadow: var(--ks-e1);
  padding: .35rem 1rem 1rem;
}
.sr-ask-head { padding: .85rem .15rem .6rem; border-bottom: 1px solid var(--ks-line); }
.sr-ask-q, .sr-ask-a {
  margin: .7rem 0 0;
  padding: .65rem .85rem;
  border-radius: var(--ks-r-md);
  font-size: .88rem;
  line-height: 1.5;
  max-width: 90%;
}
.sr-ask-q {
  margin-left: auto;
  background: var(--ks-surface-sunken);
  color: var(--ks-ink-2);
  border-bottom-right-radius: var(--ks-r-control-sm);
}
.sr-ask-a {
  margin-right: auto;
  background: color-mix(in oklab, var(--ks-accent) 8%, var(--ks-white));
  border: 1px solid color-mix(in oklab, var(--ks-accent) 18%, var(--ks-white));
  border-bottom-left-radius: var(--ks-r-control-sm);
}
/* The hand-off reads as a different KIND of answer, not a failure. Warm, never
   red: declining to guess is the product working. */
.sr-ask-a.sr-ask-hand {
  background: color-mix(in oklab, var(--ks-warn) 9%, var(--ks-white));
  border-color: color-mix(in oklab, var(--ks-warn) 26%, var(--ks-white));
}


/* ══════════════════════════════════════════════════════════════════════
   ks-comms-inbox.v1.css
   ══════════════════════════════════════════════════════════════════════ */
/* The shared inbox — rendered by ks-comms-inbox.v1.js into #comms-inbox-root.
 *
 * Tokens only, same grammar as the rest of the signed-in product. A thread row
 * is a real <button>: it is activated, it takes focus, and it must answer the
 * keyboard — the command dock's role=button defect is the local precedent for
 * why a clickable div is not acceptable here. */

.ks-comms-threads {
  display: flex;
  flex-direction: column;
  border: 1px solid var(--ks-line);
  border-radius: 12px;
  overflow: hidden;
  background: var(--ks-surface, var(--ks-canvas));
}

.ks-comms-thread {
  display: flex;
  flex-direction: column;
  gap: .2rem;
  width: 100%;
  padding: .85rem 1rem;
  border: 0;
  border-bottom: 1px solid var(--ks-line);
  background: transparent;
  text-align: left;
  cursor: pointer;
  font: inherit;
  color: inherit;
}
.ks-comms-thread:last-child { border-bottom: 0; }
.ks-comms-thread:hover { background: var(--ks-canvas); }
.ks-comms-thread:focus-visible {
  outline: 2px solid var(--ks-focus, var(--ks-accent));
  outline-offset: -2px;
}

.ks-comms-thread-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: .8rem;
}
.ks-comms-thread-who {
  font-size: .9rem;
  font-weight: 700;
  letter-spacing: -0.01em;
  color: var(--ks-ink);
  font-variant-numeric: tabular-nums;
}
.ks-comms-thread-when { font-size: .7rem; color: var(--ks-ink-3); flex: none; }

/* One line, clipped. A preview that wraps to three lines turns a scannable
   inbox into a wall, and the point of the list is to find the one that matters. */
.ks-comms-thread-preview {
  font-size: .78rem;
  color: var(--ks-ink-2);
  line-height: 1.45;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* Unread is carried by WEIGHT and a badge, not by colour alone — colour alone
   fails for a reader who cannot distinguish it, and "which of these have I not
   answered" is the single question this list exists to answer. */
.ks-comms-thread-unread .ks-comms-thread-who { font-weight: 800; }
.ks-comms-thread-unread .ks-comms-thread-preview { color: var(--ks-ink); }

.ks-comms-thread-meta {
  display: flex;
  align-items: center;
  gap: .4rem;
  margin-top: .1rem;
}
.ks-comms-thread-badge {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 1.15rem;
  height: 1.15rem;
  padding: 0 .3rem;
  border-radius: 999px;
  background: var(--ks-accent, var(--ks-ink));
  color: var(--ks-on-accent, #fff);
  font-size: .66rem;
  font-weight: 800;
  font-variant-numeric: tabular-nums;
}
.ks-comms-thread-claimed {
  font-size: .66rem;
  font-weight: 700;
  letter-spacing: .02em;
  color: var(--ks-ink-3);
  text-transform: uppercase;
}

/* ── empty and failed ────────────────────────────────────────────────────── */

.ks-comms-empty {
  display: flex;
  flex-direction: column;
  gap: .25rem;
  padding: 2rem 1.25rem;
  border: 1px dashed var(--ks-line);
  border-radius: 12px;
  text-align: center;
}
.ks-comms-empty strong { font-size: .9rem; font-weight: 800; color: var(--ks-ink); }
.ks-comms-empty span { font-size: .78rem; color: var(--ks-ink-2); line-height: 1.5; }

/* A FAILED READ MUST NOT LOOK LIKE AN EMPTY INBOX. "No messages" when the read
   actually failed tells a dojo nobody has been in touch, which is the one wrong
   thing this screen could say — so the failure state is visibly a failure. */
.ks-comms-empty-problem {
  border-style: solid;
  border-color: var(--ks-warning-line);
  background: var(--ks-warning-bg);
}
.ks-comms-empty-problem strong { color: var(--ks-warning); }

@media (max-width: 480px) {
  .ks-comms-thread { padding: .75rem .85rem; }
  .ks-comms-thread-head { gap: .5rem; }
}

/* ── the workspace: list + thread ────────────────────────────────────────────
 *
 * Desktop is two panes side by side. A PHONE IS ONE PANE AT A TIME — a 375px
 * two-pane is two unusable panes, and a shared inbox that cannot be worked from
 * a phone is not a product a dojo owner will use. The list is hidden rather than
 * shrunk when a thread is open, and a Back control appears. */

.ks-comms-workspace {
  display: grid;
  grid-template-columns: minmax(280px, 360px) minmax(0, 1fr);
  gap: 1rem;
  align-items: start;
}
/* One pane, no thread open: the list takes the width rather than leaving a
   conspicuous empty half. */
.ks-comms-workspace:not(.ks-comms-workspace-open) { grid-template-columns: minmax(0, 1fr); }

.ks-comms-pane {
  display: flex;
  flex-direction: column;
  min-height: 24rem;
  border: 1px solid var(--ks-line);
  border-radius: 12px;
  background: var(--ks-surface, var(--ks-canvas));
  overflow: hidden;
}

.ks-comms-pane-head {
  display: flex;
  align-items: center;
  gap: .6rem;
  padding: .7rem 1rem;
  border-bottom: 1px solid var(--ks-line);
}
.ks-comms-pane-who {
  font-size: .9rem;
  font-weight: 800;
  letter-spacing: -0.01em;
  color: var(--ks-ink);
  font-variant-numeric: tabular-nums;
}

/* Back exists only on the one-pane layout. Hidden with display so it leaves the
   accessibility tree too — an offered control that does nothing is worse than
   an absent one for a screen-reader user, who cannot see that it is redundant. */
.ks-comms-back {
  display: none;
  padding: .3rem .6rem;
  border: 1px solid var(--ks-line);
  border-radius: 8px;
  background: transparent;
  font: inherit;
  font-size: .75rem;
  color: var(--ks-ink-2);
  cursor: pointer;
}
.ks-comms-back:focus-visible { outline: 2px solid var(--ks-focus, var(--ks-accent)); outline-offset: 2px; }

/* ── the log ─────────────────────────────────────────────────────────────── */

.ks-comms-log {
  display: flex;
  flex-direction: column;
  gap: .5rem;
  padding: 1rem;
  flex: 1;
  overflow-y: auto;
  max-height: 32rem;
}

.ks-comms-msg {
  max-width: min(80%, 34rem);
  padding: .55rem .75rem;
  border-radius: 12px;
  background: var(--ks-canvas);
  border: 1px solid var(--ks-line);
}
.ks-comms-msg-in { align-self: flex-start; border-bottom-left-radius: 4px; }
.ks-comms-msg-out {
  align-self: flex-end;
  border-bottom-right-radius: 4px;
  background: var(--ks-accent-soft, var(--ks-canvas));
  border-color: var(--ks-line);
}
.ks-comms-msg-body {
  margin: 0;
  font-size: .82rem;
  line-height: 1.5;
  color: var(--ks-ink);
  /* A pasted address or a long URL must wrap rather than widen the pane. */
  overflow-wrap: anywhere;
  white-space: pre-wrap;
}
.ks-comms-msg-meta {
  display: flex;
  align-items: baseline;
  gap: .4rem;
  margin-top: .2rem;
  font-size: .66rem;
  color: var(--ks-ink-3);
}
/* Not colour alone: a failure is the one thing in this log an operator must
   act on, so it is also a word. */
.ks-comms-msg-failed { color: var(--ks-warning); font-weight: 700; }

/* A call or voicemail is an EVENT, centred and quiet — rendering it as a bubble
   with empty text would read as a message that failed to arrive. */
.ks-comms-event {
  align-self: center;
  padding: .2rem .6rem;
  border: 0;
  background: transparent;
  text-align: center;
}
.ks-comms-event-label {
  font-size: .7rem;
  font-weight: 700;
  letter-spacing: .03em;
  text-transform: uppercase;
  color: var(--ks-ink-3);
}

/* ── the composer ────────────────────────────────────────────────────────── */

.ks-comms-composer {
  display: flex;
  align-items: flex-end;
  gap: .5rem;
  padding: .7rem 1rem;
  border-top: 1px solid var(--ks-line);
}
.ks-comms-composer-input {
  flex: 1;
  min-width: 0;
  padding: .5rem .65rem;
  border: 1px solid var(--ks-line);
  border-radius: 8px;
  background: var(--ks-canvas);
  color: var(--ks-ink);
  font: inherit;
  font-size: .82rem;
  line-height: 1.45;
  resize: vertical;
}
.ks-comms-composer-input:focus-visible {
  outline: 2px solid var(--ks-focus, var(--ks-accent));
  outline-offset: -1px;
}
/* 44px: a send control missed on a phone sends nothing, and the retry is
   re-typing a message. */
.ks-comms-composer-send { flex: none; min-height: 44px; }

.ks-comms-thread-open { background: var(--ks-canvas); }

/* ── one pane at a time ──────────────────────────────────────────────────── */

@media (max-width: 780px) {
  .ks-comms-workspace { grid-template-columns: minmax(0, 1fr); }
  /* The thread REPLACES the list rather than stacking under it — scrolling past
     forty conversations to reach the one you opened is not a mobile layout. */
  .ks-comms-workspace-open .ks-comms-threads { display: none; }
  .ks-comms-back { display: inline-block; }
  .ks-comms-msg { max-width: 88%; }
  .ks-comms-log { max-height: none; }
}

/* Someone who asked for less motion gets none of the scroll easing the log
   would otherwise use. */
@media (prefers-reduced-motion: reduce) {
  .ks-comms-log { scroll-behavior: auto; }
}

/* Claiming — the visible half of "somebody already picked this up". */
.ks-comms-claim { display: flex; align-items: center; gap: .5rem; margin-left: auto; }
.ks-comms-claim-who { font-size: .68rem; color: var(--ks-ink-3); }
.ks-comms-claim-btn {
  min-height: 32px;
  padding: .25rem .7rem;
  border: 1px solid var(--ks-line);
  border-radius: 999px;
  background: transparent;
  font: inherit;
  font-size: .72rem;
  font-weight: 700;
  color: var(--ks-ink-2);
  cursor: pointer;
}
.ks-comms-claim-btn[aria-pressed="true"] {
  border-color: var(--ks-success-line);
  background: var(--ks-success-bg);
  color: var(--ks-success);
}
.ks-comms-claim-btn:focus-visible { outline: 2px solid var(--ks-focus, var(--ks-accent)); outline-offset: 2px; }
.ks-comms-claim-btn[disabled] { opacity: .55; cursor: default; }

/* ── tabs ────────────────────────────────────────────────────────────────── */

.ks-comms-tabs {
  display: flex;
  gap: .25rem;
  margin-bottom: .9rem;
  border-bottom: 1px solid var(--ks-line);
}
.ks-comms-tab {
  min-height: 44px;
  padding: .5rem .9rem;
  border: 0;
  border-bottom: 2px solid transparent;
  background: transparent;
  font: inherit;
  font-size: .82rem;
  font-weight: 700;
  color: var(--ks-ink-3);
  cursor: pointer;
}
.ks-comms-tab:hover { color: var(--ks-ink-2); }
.ks-comms-tab:focus-visible { outline: 2px solid var(--ks-focus, var(--ks-accent)); outline-offset: -2px; }
/* Selected is carried by weight, colour AND the underline — not colour alone,
   which is not a cue for every reader. aria-selected carries it for the rest. */
.ks-comms-tab-on {
  color: var(--ks-ink);
  font-weight: 800;
  border-bottom-color: var(--ks-accent, var(--ks-ink));
}

/* ── calls ───────────────────────────────────────────────────────────────── */

.ks-comms-filter {
  display: inline-flex;
  align-items: center;
  gap: .45rem;
  min-height: 44px;
  margin-bottom: .5rem;
  font-size: .76rem;
  color: var(--ks-ink-2);
  cursor: pointer;
}
.ks-comms-filter input { width: 16px; height: 16px; }

.ks-comms-call-list {
  display: flex;
  flex-direction: column;
  border: 1px solid var(--ks-line);
  border-radius: 12px;
  overflow: hidden;
  background: var(--ks-surface, var(--ks-canvas));
}
.ks-comms-call {
  padding: .8rem 1rem;
  border-bottom: 1px solid var(--ks-line);
  border-left: 3px solid transparent;
}
.ks-comms-call:last-child { border-bottom: 0; }
/* A missed call is the one row in this list that needs acting on, so it is
   marked structurally as well as by its word — not colour alone. */
.ks-comms-call-missed { border-left-color: var(--ks-warning); }
.ks-comms-call-missed .ks-comms-call-disposition { color: var(--ks-warning); font-weight: 800; }

.ks-comms-call-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: .8rem;
}
.ks-comms-call-who {
  font-size: .88rem;
  font-weight: 700;
  color: var(--ks-ink);
  font-variant-numeric: tabular-nums;
}
.ks-comms-call-when { font-size: .7rem; color: var(--ks-ink-3); flex: none; }
.ks-comms-call-meta {
  display: flex;
  gap: .6rem;
  margin-top: .15rem;
  font-size: .72rem;
  color: var(--ks-ink-3);
}

/* ── voicemail, inside its call ──────────────────────────────────────────── */

.ks-comms-vm {
  display: flex;
  align-items: center;
  gap: .6rem;
  flex-wrap: wrap;
  margin-top: .5rem;
  padding: .5rem .7rem;
  border: 1px solid var(--ks-line);
  border-radius: 8px;
  background: var(--ks-canvas);
}
.ks-comms-vm-head { display: flex; align-items: baseline; gap: .45rem; }
.ks-comms-vm-label {
  font-size: .68rem;
  font-weight: 800;
  letter-spacing: .04em;
  text-transform: uppercase;
  color: var(--ks-ink-3);
}
.ks-comms-vm-dur { font-size: .72rem; color: var(--ks-ink-2); font-variant-numeric: tabular-nums; }
.ks-comms-vm-play {
  min-height: 36px;
  padding: .25rem .8rem;
  border: 1px solid var(--ks-line);
  border-radius: 999px;
  background: transparent;
  font: inherit;
  font-size: .74rem;
  font-weight: 700;
  color: var(--ks-ink);
  cursor: pointer;
}
.ks-comms-vm-play:focus-visible { outline: 2px solid var(--ks-focus, var(--ks-accent)); outline-offset: 2px; }
.ks-comms-vm-play[disabled] { opacity: .55; cursor: default; }
/* The native player: keyboard, screen reader, scrubbing and speed for free, and
   a hand-rolled one would be worse at all of them. */
.ks-comms-vm-audio { flex: 1 1 14rem; min-width: 0; height: 36px; }

@media (max-width: 480px) {
  .ks-comms-vm { gap: .4rem; }
  .ks-comms-vm-audio { flex-basis: 100%; }
}

/* ── broadcast ───────────────────────────────────────────────────────────── */

.ks-comms-broadcast { max-width: 44rem; }

.ks-comms-audiences {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(12rem, 1fr));
  gap: .5rem;
  margin-bottom: .9rem;
}
.ks-comms-audience {
  display: flex;
  flex-direction: column;
  gap: .1rem;
  min-height: 60px;
  padding: .6rem .75rem;
  border: 1px solid var(--ks-line);
  border-radius: 10px;
  background: transparent;
  font: inherit;
  text-align: left;
  cursor: pointer;
}
.ks-comms-audience strong { font-size: .82rem; font-weight: 800; color: var(--ks-ink); }
.ks-comms-audience small { font-size: .69rem; color: var(--ks-ink-3); line-height: 1.4; }
.ks-comms-audience:focus-visible { outline: 2px solid var(--ks-focus, var(--ks-accent)); outline-offset: 2px; }
/* Selection carries a border AND a background, plus aria-checked — not colour
   alone, and not a hairline nobody can see on a phone in a bright dojo. */
.ks-comms-audience-on {
  border-color: var(--ks-accent, var(--ks-ink));
  border-width: 2px;
  background: var(--ks-accent-soft, var(--ks-canvas));
}

/* The count, given the weight of the thing it prevents. */
.ks-comms-preview {
  display: flex;
  flex-direction: column;
  gap: .2rem;
  padding: .75rem .9rem;
  margin-bottom: .9rem;
  border: 1px solid var(--ks-success-line);
  border-radius: 10px;
  background: var(--ks-success-bg);
}
.ks-comms-preview strong { font-size: .84rem; font-weight: 800; color: var(--ks-success); }
.ks-comms-preview span { font-size: .74rem; color: var(--ks-ink-2); line-height: 1.5; }
/* Nobody reachable, or a partial failure: the same box, told the other way. */
.ks-comms-preview-empty {
  border-color: var(--ks-warning-line);
  background: var(--ks-warning-bg);
}
.ks-comms-preview-empty strong { color: var(--ks-warning); }

.ks-comms-broadcast-input {
  width: 100%;
  padding: .6rem .7rem;
  border: 1px solid var(--ks-line);
  border-radius: 8px;
  background: var(--ks-canvas);
  color: var(--ks-ink);
  font: inherit;
  font-size: .85rem;
  line-height: 1.5;
  resize: vertical;
}
.ks-comms-broadcast-input:focus-visible {
  outline: 2px solid var(--ks-focus, var(--ks-accent));
  outline-offset: -1px;
}
/* Deliberately not full-width: a send-to-everyone control that spans the screen
   is one thumb-brush from eighty texts. */
.ks-comms-broadcast-send { min-height: 44px; margin-top: .7rem; }

/* Click-to-call, in the thread head beside the claim control. */
.ks-comms-callslot { display: flex; flex-direction: column; align-items: flex-end; gap: .15rem; }
.ks-comms-callbtn {
  min-height: 32px;
  padding: .25rem .8rem;
  border: 1px solid var(--ks-line);
  border-radius: 999px;
  background: transparent;
  font: inherit;
  font-size: .72rem;
  font-weight: 700;
  color: var(--ks-ink);
  cursor: pointer;
}
.ks-comms-callbtn:focus-visible { outline: 2px solid var(--ks-focus, var(--ks-accent)); outline-offset: 2px; }
.ks-comms-callbtn[disabled] { opacity: .55; cursor: default; }
/* Says what will physically happen next. A button reporting "started" while the
   phone has not rung yet reads as a failure. */
.ks-comms-callnote { font-size: .66rem; color: var(--ks-ink-3); max-width: 16rem; text-align: right; }

/* A send that failed, said beside the composer that tried.
   Not a pane-replacing error: the conversation is fine, the message is not, and
   the reply is still in the box waiting to be sent again. */
.ks-comms-send-error {
  flex-basis: 100%;
  margin: .4rem 0 0;
  font-size: .74rem;
  line-height: 1.45;
  color: var(--ks-warning);
}

/* A voicemail that would not play, said beside the voicemail that would not.
   Not a panel-replacing error: the call history loaded perfectly, and losing
   every call because one recording was unavailable is the wrong trade. */
.ks-comms-vm-error {
  margin: .35rem 0 0;
  font-size: .74rem;
  line-height: 1.45;
  color: var(--ks-warning);
}


/* ══════════════════════════════════════════════════════════════════════
   ks-ai-sensei-widget.v1.css
   ══════════════════════════════════════════════════════════════════════ */
/* The AI Sensei chat widget — the only AI surface an anonymous visitor sees.
 *
 * Rendered by ks-ai-sensei-widget.v1.js into #ks-sensei-widget, which the
 * script appends to <body> so the launcher floats above every view.
 *
 * DESIGN POSTURE: a quiet corner control, not a takeover. A dojo's page is
 * about the dojo, and an assistant that dims the content behind it or slides
 * across half the screen has decided it is more important than the schedule
 * somebody came to read. So: bottom-right, small, no backdrop, page stays
 * scrollable and fully usable behind it. The panel is role="dialog" and
 * deliberately NOT modal for the same reason.
 *
 * Tokens only, and the whole widget renders on the tenant's PUBLIC page, which
 * does not opt into the admin light block — so every colour here is a token
 * that already carries contrast in both schemes.
 */

.ks-sensei-widget {
  position: fixed;
  right: 1rem;
  bottom: 1rem;
  /* DELIBERATELY BELOW EVERY OVERLAY on the scale in
     dojo-global-foundation-reset-tokens.v1.css: --z-overlay 300, --z-modal 400,
     --z-toast 500. A chat launcher that floated over the open mobile menu, a
     confirmation dialog, or a save toast would be the most annoying element on
     the site — it is an assistant, and an assistant waits. High enough to clear
     page content, low enough to be covered by anything the person actually
     summoned. */
  z-index: 60;
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  gap: .5rem;
  /* The container must not intercept clicks in the gap between the panel and
     the launcher — it spans that space and the page underneath is still live. */
  pointer-events: none;
}
.ks-sensei-widget > * { pointer-events: auto; }
.ks-sensei-widget[hidden] { display: none; }

/* ── launcher ──────────────────────────────────────────────────────────── */

.ks-sensei-launcher {
  padding: .6rem 1rem;
  border: 1px solid var(--ks-accent, var(--ks-line-strong));
  border-radius: 999px;
  background: var(--ks-accent, var(--ks-ink));
  color: var(--ks-on-accent, #fff);
  font-size: .82rem;
  font-weight: 800;
  letter-spacing: -0.01em;
  cursor: pointer;
  box-shadow: 0 6px 20px rgba(0, 0, 0, .18);
}
.ks-sensei-launcher:hover { filter: brightness(1.06); }
.ks-sensei-launcher:focus-visible {
  outline: 2px solid var(--ks-focus-ring, var(--ks-accent));
  outline-offset: 2px;
}

/* ── panel ─────────────────────────────────────────────────────────────── */

.ks-sensei-w-panel {
  display: flex;
  flex-direction: column;
  width: min(22rem, calc(100vw - 2rem));
  /* THE CEILING IS WHEREVER THE WIDGET ACTUALLY SITS. The old constant
     (100vh - 6rem) assumed the default corner; once the launcher became
     draggable, a widget parked high opened a panel whose top ran off the
     screen — the owner's screenshot shows the Sensei's name and the start of
     the conversation cut clean off. --ks-sensei-clear is the widget's real
     bottom offset, published by the placement code on every move, so the
     panel can only ever fill the space that exists between it and the top of
     the viewport, less breathing room for the launcher row beneath it. */
  max-height: min(30rem, calc(100vh - var(--ks-sensei-clear, 1rem) - 4.5rem));
  border: 1px solid var(--ks-line);
  border-radius: 16px;
  background: var(--ks-surface);
  color: var(--ks-ink);
  /* Layered like elevation, not like a sticker: a tight contact shadow for
     the edge and a soft ambient one for depth — the pairing the dashboards'
     dialogs use, so the one surface a visitor meets matches the rest. */
  box-shadow: 0 1px 2px rgba(10, 16, 30, .10), 0 18px 48px rgba(10, 16, 30, .22);
  overflow: hidden;
}

/* The open is an arrival, not a pop — and only for those who want motion. */
@media (prefers-reduced-motion: no-preference) {
  .ks-sensei-w-panel {
    animation: ks-sensei-arrive .18s cubic-bezier(0.2, 0.8, 0.3, 1);
    transform-origin: bottom right;
  }
  /* Opacity and travel only — NO scale. The first version arrived at
     scale(.98), which meant every geometry read taken during those 180ms saw
     controls two percent smaller than they are; the touch-target test caught
     its own close button at 43px mid-flight. An entrance must not misreport
     the interface it is introducing. */
  @keyframes ks-sensei-arrive {
    from { opacity: 0; transform: translateY(8px); }
    to { opacity: 1; transform: none; }
  }
}

.ks-sensei-w-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: .5rem;
  padding: .6rem .5rem .6rem .9rem;
  border-bottom: 1px solid var(--ks-line);
  background: var(--ks-canvas);
}
.ks-sensei-w-ident { display: flex; flex-direction: column; gap: 1px; min-width: 0; flex: 1 1 auto; }
.ks-sensei-w-name { font-size: .88rem; font-weight: 800; letter-spacing: -0.01em; min-width: 0; }
.ks-sensei-w-sub { font-size: .64rem; font-weight: 600; letter-spacing: .06em; text-transform: uppercase; color: var(--ks-ink-3); }
.ks-sensei-w-close {
  flex: none;
  /* 44px, not 1.9rem (30px). This is the panel's only dismiss control, and a
   * 30px target is below every touch guideline there is — measured by the
   * visitor-side walk, which is the first thing to have looked at this widget
   * as a person on a phone rather than as an administrator on a laptop. The
   * glyph stays small; the button it sits in does not. */
  width: 44px;
  height: 44px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border: 1px solid transparent;
  border-radius: 8px;
  background: transparent;
  color: var(--ks-ink-2);
  font-size: 1.1rem;
  line-height: 1;
  cursor: pointer;
}
.ks-sensei-w-close:hover { border-color: var(--ks-line); color: var(--ks-ink); }
.ks-sensei-w-close:focus-visible {
  outline: 2px solid var(--ks-focus-ring, var(--ks-accent));
  outline-offset: 1px;
}

/* ── transcript ────────────────────────────────────────────────────────── */

.ks-sensei-w-log {
  flex: 1 1 auto;
  overflow-y: auto;
  /* CONTAINED, or a long answer scrolls the DOJO instead of itself: at the
     transcript's bounds iOS hands the gesture to the page, so a visitor
     reading paragraph two suddenly finds the site moving under the chat. The
     chain is cut here; the page scrolls only when the pointer is on it. */
  overscroll-behavior: contain;
  -webkit-overflow-scrolling: touch;
  padding: .9rem;
  display: flex;
  flex-direction: column;
  gap: .8rem;
}
.ks-sensei-w-greeting { margin: 0; font-size: .82rem; line-height: 1.6; color: var(--ks-ink-2); }

.ks-sensei-w-turn { display: flex; flex-direction: column; gap: .15rem; }
/* NAMED, not colour-coded. Who said what has to survive a screen reader and a
   monochrome display, so the speaker is a text label rather than a side. */
.ks-sensei-w-who {
  font-size: .66rem;
  font-weight: 800;
  letter-spacing: .04em;
  text-transform: uppercase;
  color: var(--ks-ink-3);
}
.ks-sensei-w-text { margin: 0; font-size: .84rem; line-height: 1.6; white-space: pre-wrap; }
.ks-sensei-w-turn-visitor .ks-sensei-w-text { color: var(--ks-ink-2); }
.ks-sensei-w-waiting { color: var(--ks-ink-3); font-style: italic; }
.ks-sensei-w-action { align-self: flex-start; margin-top: .35rem; }

/* ── composer ──────────────────────────────────────────────────────────── */

.ks-sensei-w-composer {
  display: flex;
  gap: .4rem;
  padding: .7rem .9rem;
  border-top: 1px solid var(--ks-line);
  background: var(--ks-canvas);
}
.ks-sensei-w-composer .ks-input {
  flex: 1 1 auto;
  min-width: 0;
  font-size: .82rem;
  /* A rounded field reads as "say something" the way a square one reads as
     "fill this in" — the one place the widget borrows messenger vocabulary. */
  border-radius: 999px;
  padding-inline: .95rem;
}
.ks-sensei-w-composer .ks-btn-primary { border-radius: 999px; }
/* SIXTEEN PIXELS OR iOS ZOOMS THE WORLD. Safari auto-zooms the entire page
   when a focused input's font is under 16px, the zoom SURVIVES closing the
   chat and even leaving the site — the owner hit Back and found the platform
   homepage magnified. Scoped to coarse pointers so desktop keeps the compact
   composer; 16px only where the zoom exists to be triggered. */
@media (pointer: coarse) {
  .ks-sensei-w-composer .ks-input { font-size: 16px; }
}
.ks-sensei-w-composer .ks-btn { flex: none; }

.ks-sensei-w-foot {
  margin: 0;
  padding: .45rem .95rem .65rem;
  background: var(--ks-canvas);
  font-size: .66rem;
  line-height: 1.45;
  color: var(--ks-ink-3);
}

/* ── phones ────────────────────────────────────────────────────────────── */

@media (max-width: 480px) {
  /* NO `left` HERE, AND NO STRETCH.
   *
   * They used to be, and between them they caused three of the four mobile
   * defects the owner reported on 2026-08-27. Pinning both edges turned the
   * mount into a stretched box whose width was a function of the drag
   * position — measured on the live site at 375px: left 9.6px, right driven
   * by JS, width 349px. So dragging the figure LEFT narrowed the widget
   * instead of moving it (at the extreme, "just a small vertical piece"), the
   * open panel inherited that width and clipped its own answer at the right
   * edge, and the clamp meant to keep the widget on screen went inert,
   * because `innerWidth - width - 8` tracked `right` exactly.
   *
   * The mount shrink-wraps its content on every screen now, exactly as it
   * always did on desktop — which is why desktop never had any of this. */
  .ks-sensei-widget { right: .6rem; bottom: .6rem; }
  .ks-sensei-launcher { align-self: flex-end; }
}

/* ── the open panel on a small screen: a sheet ──────────────────────────────
 *
 * A phone cannot afford a panel that follows a draggable object. Wherever the
 * figure was left — high, low, left, right — the conversation opens in the
 * same place: pinned to both safe edges, resting above the mobile dock, and
 * never taller than the screen it opened on. The figure keeps its freedom;
 * the reading surface stops paying for it.
 *
 * The two numbers only JS can take (the dock's real height, the visual
 * viewport) arrive as custom properties; the edges are env() insets, which
 * only CSS can read. Each side does what it alone can do. */
.ks-sensei-widget.is-sheet {
  left: max(.6rem, env(safe-area-inset-left));
  right: max(.6rem, env(safe-area-inset-right));
  bottom: calc(var(--ks-sensei-floor, 0px) + .6rem);
  align-items: stretch;
}
.ks-sensei-widget.is-sheet .ks-sensei-w-panel {
  width: auto;
  max-height: var(--ks-sensei-sheet-max, 60vh);
  /* It rises from the edge it is anchored to, not from a corner it no longer
     has any relationship with. */
  transform-origin: bottom center;
}

/* A launcher that slides or pulses is an advertisement. Respect the setting
   that says no, and keep nothing that moves without it either. */
@media (prefers-reduced-motion: reduce) {
  .ks-sensei-launcher { transition: none; }
}

/* ── the Sensei's face ─────────────────────────────────────────────────────
 *
 * Two sizes and two forms — an uploaded caricature or the server's monogram —
 * sharing one box so the layout does not shift depending on which a dojo has.
 * Both are decorative: the name sits beside them as text.
 */

.ks-sensei-w-face {
  flex: 0 0 auto;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border-radius: 50%;
  object-fit: cover;
  background: var(--ks-surface-sunken, rgba(0, 0, 0, .06));
  /* A hairline rather than a border: against a photograph it reads as a rim,
   * and against the monogram it disappears, which is what each wants. */
  box-shadow: inset 0 0 0 1px rgba(0, 0, 0, .08);
}

.ks-sensei-w-face-sm { width: 32px; height: 32px; font-size: .68rem; }
.ks-sensei-w-face-md { width: 44px; height: 44px; font-size: .84rem; }
/* The minimized widget IS the face — large enough to be the whole control. */
.ks-sensei-w-face-lg { width: 56px; height: 56px; font-size: 1rem; }

/* THE COIN. The frameless experiment before this squared every face off and
   the owner called it what it was. A cutout still wants a shape to live in —
   what it does not want is the old baked-in backdrop. So: a circular coin
   with a quiet radial ground and a hairline accent ring. The artwork is now
   the WHOLE Sensei, so the coin zooms to his head: the span clips, the image
   scales around the head's position in the 640px art. The transparent
   background lets the coin's own ground show through, which is the whole
   point of having cut it out. */
span.ks-sensei-w-face:not(.ks-sensei-w-face-monogram) {
  position: relative;
  overflow: hidden;
  background: radial-gradient(circle at 50% 30%, var(--ks-surface), var(--ks-surface-sunken));
  border: 1.5px solid color-mix(in srgb, var(--ks-accent, var(--ks-line-strong)) 45%, var(--ks-line));
  border-radius: 50%;
  box-shadow: 0 1px 3px rgba(10, 16, 30, .14);
}
.ks-sensei-w-faceimg {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
  /* Zoom to the head. In the artwork the head centers near (49%, 10%) and
     stands about 17% tall; scaling ×3.6 about that point, then sliding it to
     the coin's centre, fills roughly two-thirds of the circle with face —
     recognisable at 32px, personable at 44px. */
  transform: translate(1%, 39%) scale(3.6);
  transform-origin: 49% 10%;
  pointer-events: none;
}

.ks-sensei-w-face-monogram {
  font-weight: 800;
  letter-spacing: .01em;
  /* Quiet on purpose. This is the ABSENCE of a caricature, not a badge. */
  color: var(--ks-ink-3, #6b7280);
}

/* The launcher is a row now, not just a label. */
.ks-sensei-launcher { display: inline-flex; align-items: center; gap: .5rem; }

/* And so is the panel head, with the name taking the slack so a long dojo
 * name truncates instead of pushing the close button off the edge. */
.ks-sensei-w-head { display: flex; align-items: center; gap: .55rem; }
.ks-sensei-w-head .ks-sensei-w-name {
  flex: 1 1 auto;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* A hand-off is not an answer.
 *
 * `handoff` marks a turn where the server could not answer and said a person
 * should. It was carried on the turn and never rendered, so a refusal and a
 * confident answer looked the same. A left rule and a quiet tag — tokens only,
 * and the tag is a WORD so the distinction survives monochrome, a screen reader
 * and anyone who does not read colour as meaning. */
.ks-sensei-w-turn-handoff {
  border-left: 2px solid var(--ks-warning-line);
  padding-left: .55rem;
}

.ks-sensei-w-handoff-tag {
  font-size: .68rem;
  font-weight: 600;
  letter-spacing: .02em;
  text-transform: uppercase;
  color: var(--ks-warning);
}

/* The Sensei's own turns had no rule at all — only the visitor variant was
   styled — so this class was emitted and did nothing. */
.ks-sensei-w-turn-sensei {
  color: var(--ks-ink);
}

/* ── movable ──────────────────────────────────────────────────────────────
   The launcher is the drag handle for the whole widget; the JS clamps every
   position above the mobile dock and inside the viewport, and remembers it
   per visitor. The cursor says so on pointer devices; on touch the 8px drag
   threshold keeps a tap a tap. */
.ks-sensei-launcher {
  cursor: grab;
  /* A press-and-hold is the START OF A DRAG on this control, and iOS treats an
     un-suppressed hold as text selection — the owner moved the widget and got
     his label highlighted blue instead of a chat. Selection and the callout
     menu are meaningless on a button; off unconditionally, not only mid-drag. */
  user-select: none;
  -webkit-user-select: none;
  -webkit-touch-callout: none;
}
.ks-sensei-widget[data-ks-dragging] .ks-sensei-launcher { cursor: grabbing; }
.ks-sensei-widget[data-ks-dragging] { user-select: none; }

/* ── minimized to the face ─────────────────────────────────────────────────
   One tap on the chevron collapses the pill to just the Sensei's face — the
   owner's ask, and the right one: a visitor who wants the assistant out of
   the way keeps a recognisable, draggable, tappable presence instead of a
   vanished feature. The choice is remembered per visitor; tapping the face
   opens the chat directly, which also restores the full pill for next time. */
.ks-sensei-minimize {
  flex: none;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 30px;
  height: 30px;
  margin-left: .4rem;
  /* A SURFACE, not a ghost. The first version was transparent with
     light-theme ink — on a dojo's dark hero it was dark-on-dark, and the
     owner reported the feature as missing. A control that depends on the page
     behind it for contrast is not a control. */
  border: 1px solid var(--ks-line-strong);
  border-radius: 50%;
  background: var(--ks-surface);
  color: var(--ks-ink-2);
  cursor: pointer;
  font-size: 1rem;
  line-height: 1;
  box-shadow: 0 1px 3px rgba(10, 16, 30, .16);
}
.ks-sensei-minimize:hover { background: var(--ks-surface-sunken); color: var(--ks-ink); }
.ks-sensei-minimize:focus-visible { outline: 2px solid var(--ks-focus-ring); outline-offset: 1px; }

.ks-sensei-launcher.is-face-only {
  padding: 0;
  border-radius: 50%;
  width: 60px;
  height: 60px;
  justify-content: center;
  box-shadow: 0 1px 2px rgba(10, 16, 30, .12), 0 10px 28px rgba(10, 16, 30, .24);
}
.ks-sensei-launcher.is-face-only .ks-sensei-launcher-label { display: none; }
.ks-sensei-launcher.is-face-only .ks-sensei-w-face { margin: 0; }

/* ── the figure ────────────────────────────────────────────────────────────
 *
 * The resting state for a dojo with a caricature: the Sensei himself, no
 * pill, card or bubble. The transparent cutout is the whole control — a real
 * button whose only chrome is a contour drop-shadow, which follows the
 * artwork's edge (not its box) and is what seats him ON the page instead of
 * on top of it. Sized per device so he is recognisable without covering
 * content; the placement/drag system treats him exactly like the pill.
 */
.ks-sensei-figure {
  --ks-figure-size: 120px;
  position: relative;
  display: block;
  width: var(--ks-figure-size);
  padding: 0;
  border: 0;
  background: none;
  cursor: grab;
  user-select: none;
  -webkit-user-select: none;
  -webkit-touch-callout: none;
  -webkit-tap-highlight-color: transparent;
}
@media (min-width: 768px) { .ks-sensei-figure { --ks-figure-size: 136px; } }
@media (max-width: 480px) { .ks-sensei-figure { --ks-figure-size: 100px; align-self: flex-end; } }

.ks-sensei-figure-img {
  display: block;
  width: 100%;
  height: auto;
  pointer-events: none;
  -webkit-user-drag: none;
  filter: drop-shadow(0 2px 3px rgba(10, 16, 30, .18)) drop-shadow(0 12px 22px rgba(10, 16, 30, .22));
}
@media (hover: hover) and (prefers-reduced-motion: no-preference) {
  .ks-sensei-figure { transition: transform .16s ease; }
  .ks-sensei-figure:hover { transform: translateY(-2px); }
}
.ks-sensei-figure:focus-visible {
  outline: 2px solid var(--ks-focus-ring, var(--ks-accent));
  outline-offset: 4px;
  border-radius: 12px;
}
.ks-sensei-widget[data-ks-dragging] .ks-sensei-figure { cursor: grabbing; }

/* The wrap exists so the minimize control can sit AT the figure's shoulder
   without being inside the button. */
.ks-sensei-figure-wrap { position: relative; align-self: flex-end; }
.ks-sensei-figure-wrap .ks-sensei-minimize {
  position: absolute;
  top: 2px;
  right: -6px;
  margin: 0;
  width: 26px;
  height: 26px;
  font-size: .85rem;
  /* Present but deferential: the figure is the subject, the chip is a
     courtesy. Full strength on approach and for keyboards. */
  opacity: .55;
}
.ks-sensei-figure-wrap:hover .ks-sensei-minimize,
.ks-sensei-figure-wrap .ks-sensei-minimize:focus-visible { opacity: 1; }

/* ── tucked ────────────────────────────────────────────────────────────────
 *
 * Minimized, the figure rests against the bottom edge of the viewport — or
 * the mobile dock's top edge — with head and shoulders above it, like a
 * sensei sitting at the side of the mat. No rotation, no distortion: the box
 * shortens and the artwork keeps its own top. The placement code pins the
 * altitude; this crops the posture. Still a full-size tap target and still
 * draggable along the edge.
 */
.ks-sensei-figure.is-tucked {
  height: calc(var(--ks-figure-size) * .56);
  overflow: hidden;
}
.ks-sensei-figure.is-tucked .ks-sensei-figure-img {
  height: var(--ks-figure-size);
  width: auto;
  margin-inline: auto;
  object-fit: cover;
  object-position: 50% 0;
}
@media (hover: hover) and (prefers-reduced-motion: no-preference) {
  .ks-sensei-figure.is-tucked:hover { transform: translateY(-4px); }
}

/* ── the greeting chip ─────────────────────────────────────────────────────
 *
 * "How can I help?" — once per session, beside the resting figure, gone on
 * its own. Opacity and a few pixels of travel; under reduced motion it
 * simply appears and leaves. Pointer-events none: it is a remark, not a
 * control.
 */
.ks-sensei-hello {
  pointer-events: none;
  align-self: flex-end;
  margin-bottom: .1rem;
  padding: .4rem .75rem;
  border: 1px solid var(--ks-line);
  border-radius: 999px;
  background: var(--ks-surface);
  color: var(--ks-ink);
  font-size: .76rem;
  font-weight: 600;
  letter-spacing: -0.005em;
  box-shadow: 0 1px 2px rgba(10, 16, 30, .08), 0 6px 18px rgba(10, 16, 30, .12);
  opacity: 0;
  transform: translateY(4px);
}
.ks-sensei-hello.is-visible { opacity: 1; transform: none; }
@media (prefers-reduced-motion: no-preference) {
  .ks-sensei-hello { transition: opacity .25s ease, transform .25s ease; }
}

/* ── the open-state disclosure anchor ──────────────────────────────────────
   While the chat is open, the panel is the only visible object — but the
   launcher persists for assistive tech, reporting aria-expanded and still
   able to close. Reduced to a point rather than display:none, so it keeps
   its place in the accessibility tree and the tab order; a keyboard user
   who reaches it sees it come back as a small Close pill under the panel —
   the skip-link technique, pointed at a chat. */
.ks-sensei-launcher.ks-sensei-open-anchor {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
  box-shadow: none;
}
.ks-sensei-launcher.ks-sensei-open-anchor:focus-visible {
  position: static;
  width: auto;
  height: auto;
  padding: .6rem 1rem;
  margin: 0;
  overflow: visible;
  clip-path: none;
  border: 1px solid var(--ks-accent, var(--ks-line-strong));
}


/* ══════════════════════════════════════════════════════════════════════
   ks-progression.v1.css
   ══════════════════════════════════════════════════════════════════════ */
/* ══════════════════════════════════════════════════════════════════════════
   BELT PROGRESSION — the layout of four screens on the authenticated product
   surface: the Belt Requirements editor, the "working toward" panel (the
   dashboard's preview, Training & Progress, the profile's Progress tab), My
   Dashboard, and the video and share sheets.

   LAYOUT ONLY. Every colour, radius, shadow, control, chip, button, input,
   thumbnail and type role on these screens is the shared component layer's
   (ks-app-surface.v1.css), which reads the foundation tokens. This file
   says where things go: grids, gaps, the straddle, the rail, the breakpoints.
   A rule here that names a colour is a rule in the wrong file.

   What this file used to carry and no longer does, as of 2026-08-23: its
   own token dialect (--ks-prog-*), five button classes (.ks-prog-btn,
   .ks-vd-btn, .ks-myhub-tbtn, .ks-composer-send, .ks-reel-more), its own
   card (.ks-myhub-card/.ks-myhub-head), pills and chips, inputs, the dialog
   base, the thread and composer, and the portrait plate. Those are one
   definition each now, in the shared layer.
   ══════════════════════════════════════════════════════════════════════════ */

/* ── the editor (Admin › Settings › Belt Requirements) ─────────────────── */

.ks-curr { display: grid; gap: 1rem; }
.ks-curr-picker { padding: .9rem 1rem; }
.ks-curr-pick-field { max-width: 26rem; }
.ks-curr-editor { display: grid; gap: 1rem; }
.ks-curr-head { display: grid; gap: .35rem; }
.ks-curr-head-row { display: flex; align-items: center; gap: .6rem; flex-wrap: wrap; }
.ks-curr-lede { max-width: 62ch; }

.ks-curr-sections { display: grid; gap: .9rem; }
.ks-curr-section-head { gap: .6rem; flex-wrap: wrap; }
.ks-curr-section-head .ks-curr-section-name { flex: 1 1 14rem; min-width: 0; font-weight: 750; }
.ks-curr-controls { display: inline-flex; gap: .3rem; flex-shrink: 0; margin-left: auto; align-items: center; }
/* Reorder glyphs: one chevron, pointed the way it moves. */
.ks-curr-up .ks-icon { transform: rotate(-90deg); }
.ks-curr-down .ks-icon { transform: rotate(90deg); }

.ks-curr-items { display: grid; gap: .5rem; padding: .85rem 1.15rem 0; }
.ks-curr-item { padding: .6rem .7rem; display: grid; gap: .75rem; }
.ks-curr-item-top { display: flex; align-items: center; gap: .6rem; flex-wrap: wrap; }
.ks-curr-item-top .ks-curr-item-title { flex: 1 1 14rem; min-width: 0; font-weight: 650; }
.ks-curr-item-facts { display: inline-flex; gap: .3rem; flex-wrap: wrap; }
.ks-curr-item-more { display: grid; gap: .8rem; padding-top: .1rem; }
.ks-curr-switches { display: flex; flex-wrap: wrap; gap: .5rem 1.5rem; }
.ks-curr-switch .ks-switch-label { font-size: var(--ks-body-sm); }
.ks-curr-resources { display: grid; gap: .4rem; }
.ks-curr-resource {
  display: grid;
  grid-template-columns: minmax(12.5rem, auto) minmax(0, 1fr) minmax(9rem, .6fr) auto;
  gap: .4rem;
  align-items: center;
}
.ks-curr-resource-doc { font-size: var(--ks-body-sm); }
.ks-curr-resource-file { font: inherit; font-size: var(--ks-body-sm); color: var(--ks-ink-2); min-width: 0; }
@media (max-width: 720px) {
  .ks-curr-resource { grid-template-columns: minmax(0, 1fr) auto; }
  .ks-curr-resource-kind, .ks-curr-resource-url, .ks-curr-resource-label, .ks-curr-resource-doc { grid-column: 1 / -1; }
}
.ks-curr-section-foot { padding-top: .6rem; }

.ks-curr-add-section { padding: .9rem 1rem; display: grid; gap: .6rem; }
.ks-curr-add-section-row { display: flex; gap: .5rem; }
.ks-curr-add-section-row .ks-input { flex: 1; min-width: 0; }
.ks-curr-chips { display: flex; flex-wrap: wrap; gap: .35rem; }

.ks-curr-testing-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(12rem, 16rem));
  gap: .7rem;
}

/* The save bar: sticky to the viewport's foot so the state and the action
   are in reach however long the sheet is. */
.ks-curr-actions {
  position: sticky;
  bottom: .75rem;
  z-index: 2;
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: .6rem;
  padding: .7rem .9rem;
  background: var(--ks-surface);
  border: 1px solid var(--ks-line);
  border-radius: var(--ks-r-md);
  box-shadow: var(--ks-shadow-2);
}
.ks-curr-actions-spacer { flex: 1 1 auto; }
.ks-curr-copy-pick { width: auto; min-width: 13rem; }

/* The ladder editor beside it builds plain controls (ks-belt-ranks-admin.v1.js
   is not in this pass); until it is next touched, its fields take the shared
   look here. COMPATIBILITY ONLY — retire when that editor adopts .ks-input. */
#view-admin .ks-ranks-label,
#view-admin #ks-ranks-input,
#view-admin .ks-ranks-testing-input {
  appearance: none;
  -webkit-appearance: none;
  font: inherit;
  font-size: var(--ks-body);
  line-height: 1.35;
  padding: .55rem .75rem;
  border: 1px solid var(--ks-line-strong);
  border-radius: var(--ks-r-sm);
  background: var(--ks-surface);
  color: var(--ks-ink);
  box-sizing: border-box;
  min-width: 0;
}
#view-admin .ks-ranks-label:focus-visible,
#view-admin #ks-ranks-input:focus-visible,
#view-admin .ks-ranks-testing-input:focus-visible {
  outline: none;
  border-color: var(--ks-accent);
  box-shadow: 0 0 0 3px var(--ks-accent-soft);
}
#view-admin .ks-ranks-list { display: grid; gap: .4rem; margin-bottom: .6rem; }
#view-admin .ks-ranks-row {
  display: flex;
  align-items: center;
  gap: .6rem;
  padding: .45rem .6rem;
  border: 1px solid var(--ks-line);
  border-radius: var(--ks-r-md);
  background: var(--ks-surface);
}
#view-admin .ks-ranks-step {
  flex: 0 0 1.6rem;
  height: 1.6rem;
  display: grid;
  place-items: center;
  border-radius: 50%;
  background: var(--ks-surface-sunken);
  font-size: var(--ks-label);
  font-weight: 800;
  color: var(--ks-ink-2);
}
#view-admin .ks-ranks-label { flex: 1; font-weight: 650; }
#view-admin .ks-ranks-controls { display: inline-flex; gap: .3rem; flex-shrink: 0; }
#view-admin .ks-ranks-add { display: flex; gap: .5rem; margin: .5rem 0 .25rem; }
#view-admin .ks-ranks-add input { flex: 1; }
#view-admin .ks-ranks-save { margin-top: .6rem; }
.ks-ranks-testing { margin: .9rem 0 .5rem; padding-top: .8rem; border-top: 1px solid var(--ks-line); }
.ks-ranks-testing-label { display: block; font-size: var(--ks-body-sm); font-weight: 700; margin-bottom: .3rem; }
.ks-ranks-testing-row { display: flex; gap: .5rem; align-items: center; }
.ks-ranks-testing-note { margin: .4rem 0 0; max-width: 60ch; }

/* ── the "working toward" panel ─────────────────────────────────────────── */

.ks-prog { color: var(--ks-ink); }
.ks-prog-profile { padding: .25rem 0 1rem; }
.ks-prog-head { display: grid; gap: .3rem; margin-bottom: 1rem; }
.ks-prog-quiet-block { display: grid; gap: .35rem; }

/* The dojo's stated minimums, as labelled values. */
.ks-prog-testing { margin: 0 0 1.1rem; }
/* The dojo's minimums as one strip: cells divided by hairlines, not three
   standalone pills. */
.ks-prog-facts { display: grid; grid-template-columns: repeat(auto-fit, minmax(9rem, 1fr)); gap: 0; margin: 0; overflow: hidden; }
.ks-prog-fact { padding: .5rem .8rem; border-radius: 0; border-right: 0; }
.ks-prog-fact:first-child { border-radius: var(--ks-r-md) 0 0 var(--ks-r-md); }
.ks-prog-fact:last-child { border-radius: 0 var(--ks-r-md) var(--ks-r-md) 0; border-right: 1px solid var(--ks-line-soft); }
.ks-prog-fact .ks-kv-v { font-size: var(--ks-body-sm); }
.ks-prog-fact-v { margin: 0; }
.ks-prog-notes { margin-top: .6rem; }
.ks-prog-notes-sum { cursor: pointer; font-size: var(--ks-body-sm); font-weight: 750; color: var(--ks-ink-2); }
.ks-prog-notes-body { padding: .4rem 0 0 .2rem; font-size: var(--ks-body-sm); line-height: 1.55; color: var(--ks-ink-2); }
.ks-prog-notes-body p { margin: 0 0 .4rem; }

.ks-prog-section { margin: 0 0 1.1rem; }
.ks-prog-section-title { margin: 0 0 .5rem; }
.ks-prog-list { list-style: none; margin: 0; padding: 0; display: grid; gap: .5rem; }

.ks-prog-row {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  gap: .5rem 1rem;
  align-items: start;
  padding: .8rem .9rem;
}
.ks-prog-row.is-verified { background: var(--ks-success-bg); border-color: var(--ks-success-line); }
.ks-prog-row-main { min-width: 0; display: grid; gap: .3rem; }
.ks-prog-row-titlerow { display: flex; align-items: center; gap: .5rem; flex-wrap: wrap; }
.ks-prog-row-title { font-weight: 750; }
.ks-prog-row-detail { white-space: pre-line; }
.ks-prog-resources { display: flex; flex-wrap: wrap; gap: .4rem; margin-top: .25rem; }
.ks-prog-resources .ks-embed { flex: 1 1 100%; margin-top: .4rem; }
.ks-prog-row-side { display: flex; flex-direction: column; align-items: flex-end; gap: .45rem; min-width: 8rem; }
.ks-prog-line { grid-column: 1 / -1; }

/* The drawer: the clip, the thread, the composer, the sign-off. */
.ks-prog-drawer { grid-column: 1 / -1; margin-top: .4rem; display: grid; gap: .9rem; }
.ks-prog-submission { display: grid; gap: .45rem; }
.ks-prog-submission-meta { display: flex; align-items: center; gap: .6rem; flex-wrap: wrap; }
.ks-prog-submission-when { font-size: var(--ks-body-sm); color: var(--ks-ink-2); }
.ks-prog-submission-meta .ks-prog-link-btn { margin-left: auto; }
/* The phone's accordion control; a wide screen uses the row's own control. */
.ks-prog-row-toggle { display: none; margin-left: auto; }
.ks-prog-row-toggle .ks-icon { transform: rotate(90deg); transition: transform var(--ks-fast, .15s) var(--ease-out, ease); }
.ks-prog-row.is-open .ks-prog-row-toggle .ks-icon { transform: rotate(-90deg); }
.ks-prog-submission-note { white-space: pre-line; }
.ks-prog-earlier { border-top: 1px dashed var(--ks-line); padding-top: .6rem; }
.ks-prog-earlier-sum { cursor: pointer; font-size: var(--ks-body-sm); font-weight: 750; color: var(--ks-ink-2); }
.ks-prog-earlier .ks-prog-submission { margin-top: .8rem; }
.ks-prog-submit { display: grid; gap: .45rem; }
.ks-prog-submit .ks-btn { justify-self: start; }
.ks-prog-verify {
  display: flex;
  align-items: center;
  gap: .7rem;
  flex-wrap: wrap;
  padding-top: .6rem;
  border-top: 1px dashed var(--ks-line);
}
.ks-prog-verify .ks-btn { margin-left: auto; }

@media (max-width: 639px) {
  .ks-prog-row { grid-template-columns: minmax(0, 1fr); padding: .6rem .75rem; gap: .4rem; }
  .ks-prog-row-side { flex-direction: row; align-items: center; justify-content: space-between; min-width: 0; }
  /* ACCORDION: the title and the status stay; the description, the resources,
     the action and the attempt unfold behind the chevron. */
  .ks-prog-row-toggle { display: inline-flex; }
  /* The title and its chevron share one line; a long title wraps its own
     words, never the control. */
  .ks-prog-row-titlerow { gap: .4rem; flex-wrap: nowrap; align-items: flex-start; }
  .ks-prog-row-toggle { flex: 0 0 auto; margin-top: -.25rem; }
  .ks-prog-row-title { flex: 1 1 auto; min-width: 0; }
  .ks-prog-row:not(.is-open) .ks-prog-row-detail,
  .ks-prog-row:not(.is-open) .ks-prog-resources,
  .ks-prog-row:not(.is-open) .ks-prog-row-side .ks-prog-btn { display: none; }
  /* A closed row is one line: the title beside its status chip. */
  .ks-prog-row:not(.is-open) { grid-template-columns: minmax(0, 1fr) auto; align-items: center; gap: .5rem; }
  .ks-prog-row:not(.is-open) .ks-prog-row-side { justify-content: flex-end; min-width: 0; }
  .ks-prog-row:not(.is-open) .ks-prog-row-main { gap: 0; }
  .ks-prog-facts { grid-template-columns: repeat(3, minmax(0, 1fr)); }
  .ks-prog-fact { padding: .45rem .55rem; min-width: 0; }
  .ks-prog-fact .ks-kv-k { font-size: .62rem; letter-spacing: .06em; }
  .ks-prog-fact .ks-kv-v { font-size: .8rem; line-height: 1.25; }
  .ks-prog-testing { margin-bottom: .8rem; }
  .ks-prog-section { margin-bottom: .8rem; }
  .ks-prog-section-title { margin-bottom: .35rem; }
  .ks-prog-list { gap: .35rem; }
  .ks-training-card { padding: .9rem .8rem; }
  .ks-prog-head { margin-bottom: .6rem; }
}

/* The dashboard's preview of the same panel: the rank, the count, one chip
   row, the route out. */
.ks-prog-preview { display: grid; gap: .7rem; align-content: start; }
.ks-prog-preview .ks-prog-head { margin-bottom: 0; gap: .2rem; }
.ks-prog-preview .ks-prog-kicker { display: none; }
/* The preview's groups: one per configured section, the requirement names
   as a compact wrapped line with a status mark each. */
.ks-prog-pre-groups { list-style: none; margin: 0; padding: 0; display: grid; gap: .55rem; }
.ks-prog-pre-group { display: grid; gap: .25rem; }
.ks-prog-pre-group + .ks-prog-pre-group { border-top: 1px solid var(--ks-line-soft); padding-top: .55rem; }
.ks-prog-pre-group-head { display: flex; align-items: baseline; gap: .5rem; flex-wrap: wrap; }
.ks-prog-pre-group-t { font-weight: 750; font-size: var(--ks-body-sm); }
.ks-prog-pre-names { list-style: none; margin: 0; padding: 0; display: flex; flex-wrap: wrap; gap: .2rem .7rem; }
.ks-prog-pre-chip { display: inline-flex; align-items: baseline; gap: .35rem; font-size: var(--ks-body-sm); color: var(--ks-ink-2); min-width: 0; }
.ks-prog-pre-chip.is-verified { color: var(--ks-ink); }
/* Whole names: a requirement the dojo wrote is shown as written. */
.ks-prog-pre-chip .ks-prog-pre-name { line-height: 1.3; }
.ks-prog-pre-chip .ks-chip-mark { position: relative; top: -1px; }
.ks-prog-pre-more { align-self: center; }
.ks-prog-open { justify-self: start; margin-top: auto; }

/* ══ MY DASHBOARD ══════════════════════════════════════════════════════════
   A DASHBOARD, not a document: the tenant's band carrying the athlete, the
   rank strip straddling its foot, then modules of three tiers on the light
   surface. Three compositions — under 640, 640–959, 960 and up — and source
   order is the phone's order.
   ══════════════════════════════════════════════════════════════════════════ */

#view-my-dashboard .ks-app,
#view-training .ks-app {
  --ks-portrait-w: clamp(176px, 15.5vw, 232px);
  --ks-myhub-straddle: clamp(2.4rem, 3.6vw, 3.2rem);
}

/* The stage carries the band; the strip hangs out of the band's foot by the
   straddle, and the stage's own margin keeps the body clear of it. The band
   is a grid so the strip's negative margin shortens the BAND rather than
   collapsing out through it — which is the whole straddle (measured). */
.ks-myhub-stage { margin-bottom: var(--ks-myhub-straddle); }
.ks-myhub-stage-flat { margin-bottom: 0; }
.ks-myhub-hero-band { display: grid; grid-template-columns: minmax(0, 1fr); }

/* Two rows: the copy, then the actions. The first row takes whatever height
   the portrait (spanning both) leaves, and the copy sits at its foot, so the
   buttons stand directly under the name however tall the plate is. */
.ks-myhub-hero {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  grid-template-rows: 1fr auto;
  align-items: end;
  /* The column gap keeps the portrait off the copy; the row gap is only the
     breath between the name line and its buttons. */
  column-gap: clamp(1.5rem, 4vw, 3rem);
  row-gap: .9rem;
  padding-block: clamp(1.4rem, 2.6vw, 2rem) clamp(1.1rem, 2.2vw, 1.6rem);
}
/* Every hero child is placed explicitly: an item with a definite row and an
   auto column is placed BEFORE fully-auto items, which put the portrait in
   the first column on every width (measured). */
.ks-myhub-hero-copy { grid-column: 1; grid-row: 1; min-width: 0; display: grid; gap: .55rem; justify-items: start; }
.ks-myhub-hero-line { display: flex; align-items: center; gap: .6rem; flex-wrap: wrap; margin: 0; }
/* The belt, as a belt: the dojo's own colour on a light plate so a black or
   brown rank is legible on a dark band. */
.ks-myhub-belt { display: inline-flex; align-items: center; gap: .45rem; font-size: var(--ks-body-sm); font-weight: 800; color: var(--ks-band-ink); }
.ks-myhub-belt::before {
  content: '';
  width: 22px;
  height: 8px;
  border-radius: 2px;
  background: var(--ks-myhub-belt, var(--ks-band-line));
  box-shadow: 0 0 0 1.5px var(--ks-band-ink);
}
.ks-myhub-dojo { font-size: var(--ks-body-sm); color: var(--ks-band-ink-2); }
.ks-myhub-dojo::before { content: '·'; margin-right: .6rem; opacity: .6; }
.ks-myhub-hero-actions { grid-column: 1; grid-row: 2; display: flex; gap: .55rem; flex-wrap: wrap; }
.ks-myhub-portrait { grid-column: 2; grid-row: 1 / span 2; justify-self: end; position: relative; z-index: 3; }
.ks-myhub-switch { display: inline-flex; align-items: center; gap: .55rem; padding-bottom: 1rem; }
.ks-myhub-switch-k { color: var(--ks-band-ink-2); }
.ks-myhub-switch-select { width: auto; min-width: 9rem; padding-right: 2rem; }

/* The rank strip, straddling the band's foot. */
.ks-myhub-rank {
  position: relative;
  z-index: 2;
  width: 100%;
  max-width: var(--ks-measure);
  margin: 0 auto calc(-1 * var(--ks-myhub-straddle));
  padding-inline: var(--ks-gutter);
}
.ks-myhub-rank-inner {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(0, 1fr));
  color: var(--ks-ink);
}
.ks-myhub-rank-cell {
  display: flex;
  align-items: stretch;
  gap: .85rem;
  padding: clamp(.95rem, 2vw, 1.3rem) clamp(1rem, 2.2vw, 1.5rem);
  border-left: 1px solid var(--ks-line-soft);
  min-width: 0;
}
.ks-myhub-rank-cell:first-child { border-left: 0; }
/* The belt's colour as a bar beside the rank it names — the one place colour
   is used as data on this surface. */
.ks-myhub-rank-swatch { flex: 0 0 auto; width: 6px; border-radius: 3px; background: var(--ks-myhub-belt, var(--ks-line)); }
.ks-myhub-rank-cell.is-test .ks-kv-v { color: var(--ks-accent-strong); }

/* Up next: one line, derived from the record, gone when nothing qualifies. */
.ks-myhub-upnext { display: flex; flex-direction: row; align-items: center; gap: .75rem; padding: .6rem 1rem; flex-wrap: wrap; }
.ks-myhub-upnext-k { flex: 0 0 auto; }
.ks-myhub-upnext-t { flex: 1 1 14rem; min-width: 0; font-weight: 650; }
.ks-myhub-upnext .ks-btn { margin-left: auto; }

/* The dashboard grid: the shared twelve columns; each module names its span. */
.ks-myhub-grid { align-items: stretch; }

/* ── coming up ── */
.ks-myhub-events { list-style: none; margin: 0; padding: 0; }
.ks-myhub-event { display: flex; align-items: center; gap: .85rem; padding: .8rem 1.15rem; border-top: 1px solid var(--ks-line-soft); }
.ks-myhub-event:first-child { border-top: 0; }
.ks-myhub-date { flex: 0 0 auto; display: grid; place-items: center; width: 3rem; padding: .3rem 0 .35rem; border-radius: var(--ks-r-sm); background: var(--ks-surface-sunken); border: 1px solid var(--ks-line-soft); line-height: 1; }
.ks-myhub-date-m { font-size: var(--ks-label); font-weight: 850; letter-spacing: .08em; color: var(--ks-ink-3); }
.ks-myhub-date-d { font-family: var(--ks-display); font-size: 1.2rem; font-weight: 700; }
.ks-myhub-event-copy { display: grid; gap: .12rem; min-width: 0; flex: 1; }
.ks-myhub-event-t { font-weight: 750; line-height: 1.3; }

/* ── next tournament ── */
.ks-myhub-tbody { gap: .85rem; }
.ks-myhub-tfacts { display: grid; grid-template-columns: repeat(auto-fit, minmax(10rem, 1fr)); gap: .7rem 1rem; }
.ks-myhub-tfact { display: flex; align-items: flex-start; gap: .55rem; min-width: 0; }
.ks-myhub-tfact-i { display: inline-flex; width: 1.05rem; height: 1.05rem; margin-top: .15rem; color: var(--ks-accent-strong); flex: 0 0 auto; }
.ks-myhub-tfact-i svg, .ks-myhub-tcoach-i svg, .ks-myhub-icon svg { width: 100%; height: 100%; }
.ks-myhub-icon { display: inline-flex; width: 1.05rem; height: 1.05rem; color: var(--ks-accent-strong); }
.ks-myhub-tcoach { display: flex; gap: .6rem; padding: .75rem .9rem; border-left: 3px solid var(--ks-accent); }
.ks-myhub-tcoach-i { display: inline-flex; width: 1.05rem; height: 1.05rem; color: var(--ks-accent-strong); flex: 0 0 auto; margin-top: .15rem; }
.ks-myhub-tcoach-copy { display: grid; gap: .15rem; }
/* The Tournament Hub's RSVP control, in its own markup and the hub's own
   styles (.ksh-rsvp / .ksh-seg) — seated here as ONE compact row: the question
   and the three answers side by side, a fact to confirm rather than a form. */
.ks-myhub-trsvp .ksh-rsvp { margin-top: 0; padding: .45rem .6rem .45rem .8rem; display: flex; align-items: center; gap: .6rem 1rem; flex-wrap: wrap; }
.ks-myhub-trsvp .ksh-rsvp-k { margin: 0; font-size: var(--ks-body-sm); flex: 1 1 auto; }
.ks-myhub-trsvp .ksh-seg { margin-top: 0; flex: 0 1 18rem; min-width: 14rem; padding: 3px; gap: 3px; }
.ks-myhub-trsvp .ksh-seg button { min-height: 2rem; padding: .3rem .5rem; font-size: var(--ks-label); }

/* ── my videos ── */
.ks-reel { display: grid; gap: .75rem; padding: .9rem 1.15rem 1.15rem; --ks-rail-min: 11.5rem; }
.ks-reel-filters { display: flex; flex-wrap: wrap; gap: .35rem; }
.ks-reel-card { text-align: left; padding: 0 0 .7rem; font: inherit; gap: .5rem; }
.ks-reel-body { display: grid; gap: .25rem; padding-inline: .7rem; min-width: 0; align-content: start; justify-items: start; }
.ks-reel-title { font-weight: 750; line-height: 1.3; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-width: 100%; }
.ks-reel-share-art { display: grid; place-items: center; }
.ks-reel-share-plus {
  display: grid;
  place-items: center;
  width: 3rem;
  height: 3rem;
  border-radius: 50%;
  background: var(--ks-accent);
  color: var(--ks-on-accent);
  font-family: var(--ks-display);
  font-size: 1.9rem;
  font-weight: 700;
  line-height: 1;
  box-shadow: var(--ks-shadow-2);
}
.ks-reel-share { border-style: dashed; }
.ks-reel-more { justify-self: start; }

/* ── belt resources ── */
.ks-myhub-resgrid { display: grid; grid-template-columns: repeat(auto-fill, minmax(min(100%, 7.8rem), 1fr)); gap: .6rem; }
.ks-myhub-rescard { display: grid; gap: .45rem; padding: .5rem; min-width: 0; }
.ks-myhub-resart { border-radius: var(--ks-r-sm); }
.ks-myhub-rescopy { display: grid; gap: .2rem; min-width: 0; justify-items: start; }
.ks-myhub-rescopy .ks-prog-res { max-width: 100%; white-space: normal; text-align: left; line-height: 1.25; }

/* ── recent coaching ── */
.ks-myhub-notes { list-style: none; margin: 0; padding: 0; }
.ks-myhub-note { border-top: 1px solid var(--ks-line-soft); }
.ks-myhub-note:first-child { border-top: 0; }
.ks-myhub-note-btn {
  display: flex;
  gap: .75rem;
  width: 100%;
  padding: .75rem 1.15rem;
  background: none;
  border: 0;
  text-align: left;
  font: inherit;
  color: inherit;
  cursor: pointer;
}
.ks-myhub-note-btn:hover { background: var(--ks-surface-sunken); }
.ks-myhub-note-art { flex: 0 0 4.8rem; border-radius: var(--ks-r-xs); }
.ks-myhub-note-copy { display: grid; gap: .15rem; min-width: 0; }
.ks-myhub-note-head { display: flex; align-items: baseline; gap: .5rem; flex-wrap: wrap; }
.ks-myhub-note-who { font-weight: 800; }
.ks-myhub-note-text { display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; }

/* ── belt journey ── */
.ks-myhub-ladder {
  list-style: none;
  margin: 0;
  padding: 1rem 1.15rem 1.1rem;
  display: flex;
  align-items: flex-start;
  overflow-x: auto;
  scrollbar-width: thin;
}
.ks-myhub-rung {
  position: relative;
  /* Never narrower than its own label: a rung that shrank under "White
     Belt" clipped the first word off the scroll start (measured, 375px). */
  flex: 1 0 auto;
  min-width: max-content;
  padding-inline: .4rem;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: .45rem;
  text-align: center;
  font-size: var(--ks-label);
  font-weight: 750;
  color: var(--ks-ink-3);
}
.ks-myhub-rung::before { content: ''; position: absolute; top: 7px; left: 50%; width: 100%; height: 2px; background: var(--ks-line); }
.ks-myhub-rung:last-child::before { display: none; }
.ks-myhub-rung.is-done::before { background: var(--ks-ink-3); }
.ks-myhub-rung-dot { position: relative; z-index: 1; width: 16px; height: 16px; border-radius: 50%; border: 2px solid var(--ks-line); background: var(--ks-surface); }
.ks-myhub-rung.is-done .ks-myhub-rung-dot { background: var(--ks-myhub-belt, var(--ks-ink-3)); border-color: color-mix(in oklab, var(--ks-myhub-belt, var(--ks-ink-3)) 70%, var(--ks-ink) 10%); }
.ks-myhub-rung.is-current { color: var(--ks-ink); font-weight: 850; }
.ks-myhub-rung.is-current .ks-myhub-rung-dot { width: 19px; height: 19px; background: var(--ks-myhub-belt, var(--ks-accent)); border-color: var(--ks-band-deep); box-shadow: 0 0 0 4px var(--ks-accent-soft); }
.ks-myhub-rung.is-next { color: var(--ks-ink-2); }
.ks-myhub-rung.is-next .ks-myhub-rung-dot { border-color: var(--ks-accent); border-style: dashed; }
.ks-myhub-rung-label { max-width: 6.5rem; line-height: 1.2; white-space: nowrap; }

/* ── access states ── */
.ks-myhub-access-inner { max-width: 34rem; }
.ks-myhub-access-inner .ks-card-body { justify-items: start; gap: .6rem; }

/* ── Training & Progress ── */
.ks-training-head { padding-block: clamp(1.4rem, 2.6vw, 2.2rem) clamp(1.4rem, 2.6vw, 2rem); }
.ks-training-copy { display: grid; gap: .45rem; justify-items: start; }
.ks-training-line { display: flex; align-items: center; gap: .7rem; flex-wrap: wrap; margin: 0; }
.ks-training-band .ks-myhub-switch { padding-bottom: clamp(1.1rem, 2vw, 1.5rem); }
.ks-training-card { padding: clamp(1.1rem, 2.4vw, 1.6rem); }

/* ── wide: the straddle ──────────────────────────────────────────────────── */
@media (min-width: 960px) {
  .ks-myhub-hero { grid-template-columns: minmax(0, 1fr) var(--ks-portrait-w); padding-bottom: 1.1rem; }
  .ks-myhub-hero-actions { padding-bottom: 1.2rem; }
  /* THE STRADDLE: the portrait reaches past the hero, past the switcher, and
     down over the strip, seated a little above the strip's foot. */
  .ks-myhub-portrait { margin-bottom: -6.6rem; }
  .ks-myhub-hero-band:has(.ks-myhub-switch) .ks-myhub-portrait { margin-bottom: -9.4rem; }
  /* The strip reserves the seat, and the seat is part of the strip. */
  .ks-myhub-hero-band:has(.ks-myhub-portrait) .ks-myhub-rank-inner { padding-right: calc(var(--ks-portrait-w) + 2.4rem); }
  .ks-myhub-rank-inner { grid-template-columns: repeat(3, minmax(0, 1fr)); }
}

/* ── phone: recomposed, not squeezed ────────────────────────────────────── */
@media (max-width: 639px) {
  #view-my-dashboard .ks-app, #view-training .ks-app { --ks-portrait-w: 48px; --ks-myhub-straddle: 1.5rem; }
  .ks-myhub-portrait, .ks-myhub-portrait-plate { aspect-ratio: 1; }
  .ks-myhub-hero {
    /* The portrait moves BESIDE the name rather than above it: on a phone the
       first screen has to carry who you are and where you stand. */
    grid-template-columns: auto minmax(0, 1fr);
    grid-template-rows: auto auto;
    align-items: center;
    gap: .25rem .65rem;
    padding-block: .5rem .45rem;
  }
  .ks-myhub-portrait { grid-column: 1; grid-row: 1; justify-self: start; }
  .ks-myhub-hero-copy { grid-column: 2; grid-row: 1; gap: .4rem; }
  .ks-myhub-hero-actions { grid-column: 1 / -1; grid-row: 2; width: 100%; }
  .ks-myhub-hero-actions .ks-btn { flex: 1; }
  .ks-myhub-switch { grid-column: 1 / -1; padding-bottom: .8rem; }
  /* Two facts across, the third full width beneath: three columns on a phone
     turns a rank name into two characters per line. */
  .ks-myhub-rank-inner { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .ks-myhub-rank-cell { padding: .8rem .9rem; gap: .6rem; }
  .ks-myhub-rank-cell.is-test { grid-column: 1 / -1; border-left: 0; border-top: 1px solid var(--ks-line-soft); }
  .ks-myhub-rank-cell.is-test .ks-kv { display: flex; align-items: baseline; gap: .5rem; flex-wrap: wrap; }
  .ks-myhub-event, .ks-myhub-note-btn { padding-inline: .9rem; }
  .ks-reel { padding-inline: .9rem; }
  /* The tournament's facts as single lines on a phone — key and value side
     by side, the icon leading — instead of three stacked two-line blocks. */
  .ks-myhub-tfacts { grid-template-columns: minmax(0, 1fr); gap: .35rem; }
  .ks-myhub-tfact { align-items: baseline; gap: .45rem; }
  .ks-myhub-tfact-i { margin-top: 0; align-self: center; }
  .ks-myhub-tfact .ks-kv { display: flex; align-items: baseline; gap: .4rem; flex-wrap: wrap; }
  .ks-myhub-tfact .ks-kv-k { flex: 0 0 auto; font-size: .62rem; }
  .ks-myhub-tfact .ks-kv-v { font-size: var(--ks-body-sm); }
  .ks-myhub-tbody { gap: .65rem; }
  .ks-myhub-tcover { padding: .85rem .9rem; }
  .ks-myhub-tcoach { padding: .6rem .75rem; }
  .ks-training-head { padding-block: .6rem .65rem; }
  .ks-training-head .ks-kicker { display: none; }
  .ks-training-title { font-size: 1.4rem; line-height: 1.1; }
  .ks-training-copy { gap: .25rem; }
  .ks-training-band .ks-myhub-switch { padding-bottom: .5rem; }
  .ks-training-card .ks-prog-head { margin-bottom: .5rem; gap: .1rem; }
  .ks-training-card .ks-prog-title { font-size: 1.2rem; }

  /* The hero, compressed: a small plate beside the name, the kicker gone, one
     small primary action and the quiet profile link on a single row. */
  .ks-myhub-hero .ks-kicker { display: none; }
  .ks-myhub-name { font-size: clamp(1.3rem, 6vw, 1.55rem); line-height: 1.15; }
  .ks-myhub-hero-copy { gap: .2rem; }
  .ks-myhub-hero-line { gap: .4rem; }
  .ks-myhub-belt, .ks-myhub-dojo { font-size: .8rem; }
  .ks-myhub-hero-actions { display: flex; align-items: center; gap: .35rem; }
  .ks-myhub-hero-actions .ks-btn { flex: 0 1 auto; }
  .ks-myhub-switch { padding-bottom: .5rem; }
  /* The strip: two facts across, the test as one line beneath — each cell
     shorter, the values still the largest type on the first screen. */
  .ks-myhub-rank-cell { padding: .45rem .75rem; gap: .5rem; }
  .ks-myhub-rank-cell .ks-kv { gap: .05rem; }
  .ks-myhub-rank-cell .ks-kv-k { font-size: .6rem; }
  .ks-myhub-rank-cell .ks-kv-v { font-size: 1rem; line-height: 1.2; }
  .ks-myhub-rank-cell.is-test { padding-block: .35rem; }
  .ks-myhub-rank-cell.is-test .ks-kv-v { font-size: .95rem; }
  .ks-myhub-rank-cell.is-test .ks-kv-s { font-size: .75rem; }
  .ks-myhub-rank-swatch { width: 4px; }
  .ks-myhub-stage { margin-bottom: var(--ks-myhub-straddle); }
  /* Up next on a phone: one line — the text and its action, no kicker. */
  .ks-myhub-upnext { padding: .4rem .5rem .4rem .8rem; gap: .5rem; flex-wrap: nowrap; }
  .ks-myhub-upnext-k { display: none; }
  .ks-myhub-upnext-t { flex: 1 1 auto; min-width: 0; font-size: var(--ks-body-sm); }
  .ks-myhub-upnext .ks-btn { flex: 0 0 auto; }

  /* Film as rows: a thumbnail beside its title, full width, one under the
     next — the shape a phone reads, instead of half-width tiles. */
  .ks-reel-strip { display: grid; grid-auto-flow: row; grid-template-columns: minmax(0, 1fr); grid-auto-columns: auto; gap: .5rem; overflow: visible; padding-bottom: 0; }
  .ks-reel-strip > * { scroll-snap-align: none; }
  .ks-reel-card { display: grid; grid-template-columns: 8.25rem minmax(0, 1fr); align-items: center; gap: .7rem; padding: .4rem .6rem .4rem .4rem; min-height: 5.75rem; }
  .ks-reel-art { border-radius: var(--ks-r-sm); }
  .ks-reel-body { padding-inline: 0; }
  .ks-reel-share-plus { width: 2.25rem; height: 2.25rem; font-size: 1.4rem; }

  /* Belt resources as rows too. */
  .ks-myhub-resgrid { grid-template-columns: minmax(0, 1fr); gap: .4rem; }
  .ks-myhub-rescard { grid-template-columns: 4.75rem minmax(0, 1fr); align-items: center; gap: .6rem; padding: .4rem; }

  /* The journey: previous, current and next, whole labels; the card's link
     is the full history. */
  .ks-myhub-ladder { padding: .9rem .9rem 1rem; overflow: visible; }
  .ks-myhub-rung:not(.is-prev):not(.is-current):not(.is-next) { display: none; }
  .ks-myhub-rung { flex: 1 1 0; min-width: 0; }
  .ks-myhub-rung.is-next::before { display: none; }
  .ks-myhub-rung-label { white-space: normal; max-width: 7rem; }
}

/* ══ THE SHEETS — the video detail and the share form ═════════════════════ */
.ks-vd-kindrow { display: flex; align-items: center; gap: .4rem; flex-wrap: wrap; }
.ks-vd-facts { display: grid; grid-template-columns: repeat(auto-fit, minmax(8rem, 1fr)); gap: .5rem; }
.ks-vd-fact { padding: .6rem .8rem; }
.ks-vd-note { grid-column: 1 / -1; margin: 0; padding: .75rem .9rem; border-left: 3px solid var(--ks-accent); white-space: pre-line; }
.ks-vd-thread .ks-card-body { gap: .6rem; }
.ks-vd-actions { display: flex; gap: .55rem; flex-wrap: wrap; }
.ks-share-form { display: grid; gap: .9rem; }
.ks-share-cats { border: 0; padding: 0; margin: 0; }
.ks-share-row { display: flex; align-items: center; gap: .8rem; flex-wrap: wrap; }
.ks-share-row .ks-status { flex: 1 1 12rem; }
.ks-share-send { margin-left: auto; }

@media (prefers-reduced-motion: reduce) {
  .ks-myhub-ladder { scroll-behavior: auto; }
}


/* ══════════════════════════════════════════════════════════════════════
   ks-attendance.v1.css
   ══════════════════════════════════════════════════════════════════════ */
/* ══════════════════════════════════════════════════════════════════════════
   CLASSES & ATTENDANCE — the layout of two screens: the register (a view) and
   the class editor (Admin › Settings › Classes).

   LAYOUT ONLY, the same discipline as ks-progression.v1.css beside it. Every
   colour, radius, shadow, button, chip, input and type role here belongs to the
   shared component layer (ks-app-surface.v1.css) reading the foundation tokens.
   A rule in this file that names a colour is a rule in the wrong file — the two
   exceptions below say why they are exceptions.

   THE ONE THING THIS SCREEN MUST GET RIGHT is the register on a phone held in
   one hand at the side of a mat. Three targets per row, each comfortably past
   the 44px floor, no horizontal scroll, and the save control always reachable
   without hunting. Everything else follows from that.
   ══════════════════════════════════════════════════════════════════════════ */

/* ── the register ───────────────────────────────────────────────────────── */

.ks-att-datebar {
  display: flex;
  align-items: center;
  gap: .6rem;
  flex-wrap: wrap;
  margin-bottom: 1rem;
}

.ks-att-date {
  /* 16px is the iOS zoom floor: anything smaller and tapping the field zooms
     the page, which on this screen throws the coach out of the roster they
     were halfway through. The repo-wide rule, applied here deliberately. */
  font-size: max(1rem, var(--ks-body-sm));
  padding: .5rem .65rem;
  border-radius: var(--ks-radius-sm, 8px);
  border: 1px solid var(--ks-line);
  background: var(--ks-surface);
  color: var(--ks-ink);
  min-height: 44px;
}

.ks-att-dayname { font-weight: 650; }

.ks-att-classes { display: grid; gap: .75rem; }

.ks-att-class { display: grid; gap: .5rem; padding: 1rem 1.15rem; }
.ks-att-class-head { display: grid; gap: .25rem; }
.ks-att-class-title { display: flex; align-items: center; gap: .5rem; flex-wrap: wrap; }
.ks-att-class-name { font-weight: 700; }
.ks-att-chip { flex-shrink: 0; }
.ks-att-class-meta { margin: 0; }
.ks-att-class-state { margin: 0; font-size: var(--ks-body-sm); color: var(--ks-ink-2); }
.ks-att-class-state.is-marked { color: var(--ks-ink); font-weight: 650; }
.ks-att-cancelled { margin: 0; }

.ks-att-class .ks-btn { justify-self: start; }

/* ── one class's roster ─────────────────────────────────────────────────── */

.ks-att-roster {
  display: grid;
  gap: .6rem;
  margin-top: .35rem;
  padding-top: .85rem;
  border-top: 1px solid var(--ks-line);
}

.ks-att-tools { display: flex; gap: .5rem; flex-wrap: wrap; align-items: center; }

.ks-att-search {
  flex: 1 1 12rem;
  min-width: 0;
  font-size: max(1rem, var(--ks-body-sm));
  padding: .5rem .65rem;
  border-radius: var(--ks-radius-sm, 8px);
  border: 1px solid var(--ks-line);
  background: var(--ks-surface);
  color: var(--ks-ink);
  min-height: 44px;
}

.ks-att-rows { display: grid; gap: .35rem; }

/* The row is a two-part grid that becomes one column on a phone. `minmax(0,
   1fr)` rather than a bare `1fr`: a bare one is minmax(AUTO, 1fr), so a long
   name would push the marking controls off the screen instead of wrapping —
   the mobile blowout class this repository has already met once. */
.ks-att-row {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  align-items: center;
  gap: .5rem .75rem;
  padding: .55rem .65rem;
  border-radius: var(--ks-radius-sm, 8px);
  border: 1px solid var(--ks-line);
}

/* The one colour rule in this file, and it earns its place: an unanswered row
   and an answered one are the only two states a coach scans for, and the scan
   has to work at arm's length on a phone. A tint, not a border, so the row's
   height does not shift as it is marked and the list stops jumping under the
   thumb. */
.ks-att-row.is-answered { background: var(--ks-canvas-warm); }

.ks-att-who { display: grid; gap: .1rem; min-width: 0; }
.ks-att-name {
  font-weight: 650;
  /* A long name wraps rather than truncating: this is the one thing on the row
     that has to be readable, and an ellipsis on two students with the same
     first name makes the register unusable. */
  overflow-wrap: anywhere;
}
.ks-att-belt { margin: 0; }
.ks-att-source { grid-column: 1 / -1; margin: 0; }

.ks-att-marks { display: inline-flex; gap: .25rem; flex-shrink: 0; }

.ks-att-mark {
  min-height: 44px;
  min-width: 44px;
  padding: .4rem .7rem;
  border-radius: var(--ks-radius-sm, 8px);
  border: 1px solid var(--ks-line);
  background: var(--ks-surface);
  color: var(--ks-ink-2);
  font: inherit;
  font-size: var(--ks-body-sm);
  font-weight: 650;
  cursor: pointer;
}

/* The second colour rule, and the same justification: which of three answers is
   selected must be unmistakable, and a border-only selection is not — it is the
   state a coach is checking when they look back over a marked register.
   Selection reads from the accent the tenant already carries, so a dojo's own
   theme is what highlights it. */
.ks-att-mark.is-on {
  background: var(--ks-accent);
  border-color: var(--ks-accent);
  color: var(--ks-accent-ink, #fff);
}

.ks-att-mark:focus-visible { outline: 2px solid var(--ks-focus, var(--ks-accent)); outline-offset: 2px; }

.ks-att-save {
  display: flex;
  align-items: center;
  gap: .6rem;
  flex-wrap: wrap;
  padding-top: .5rem;
  border-top: 1px solid var(--ks-line);
}
.ks-att-unsaved { margin: 0; }

.ks-att-empty, .ks-att-status { margin: .5rem 0 0; }
.ks-att-status.is-error { color: var(--ks-danger, var(--ks-ink)); font-weight: 650; }
.ks-att-empty-state, .ks-att-denied { display: grid; gap: .35rem; padding: 1.25rem; }
.ks-att-empty-state p, .ks-att-denied p { margin: 0; }

@media (max-width: 560px) {
  /* One column: the name above its three controls, so a long name never
     competes with them for width and the targets keep their full size. */
  .ks-att-row { grid-template-columns: minmax(0, 1fr); }
  .ks-att-marks { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); }
  .ks-att-mark { width: 100%; }
  .ks-att-save { position: sticky; bottom: 0; background: var(--ks-surface); padding-bottom: .5rem; }
}

/* ── the class editor (Admin › Settings › Classes) ──────────────────────── */

.ks-cls-list { display: grid; gap: .6rem; margin-bottom: .9rem; }

.ks-cls-row {
  display: grid;
  gap: .3rem;
  padding: .75rem .9rem;
  border-radius: var(--ks-radius-sm, 8px);
  border: 1px solid var(--ks-line);
}
.ks-cls-row.is-broken { border-style: dashed; }
.ks-cls-row-head { display: flex; align-items: center; gap: .5rem; flex-wrap: wrap; }
.ks-cls-row-name { font-weight: 700; }
.ks-cls-chip { flex-shrink: 0; }
.ks-cls-row-meta, .ks-cls-row-dates { margin: 0; font-size: var(--ks-body-sm); }
.ks-cls-row-actions { display: flex; gap: .35rem; flex-wrap: wrap; margin-top: .35rem; }

.ks-cls-form {
  display: grid;
  gap: .9rem;
  padding: 1rem;
  margin-top: .5rem;
  border-radius: var(--ks-radius-sm, 8px);
  border: 1px solid var(--ks-line);
}
.ks-cls-sub { margin: 0; font-size: var(--ks-body); font-weight: 700; }

.ks-cls-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 13rem), 1fr));
  gap: .75rem;
}
.ks-cls-field { display: grid; gap: .25rem; align-content: start; }
.ks-cls-field-label { font-size: var(--ks-body-sm); font-weight: 650; }
.ks-cls-field-hint { margin: 0; font-size: var(--ks-body-sm); }

.ks-cls-input {
  font-size: max(1rem, var(--ks-body-sm));
  padding: .5rem .65rem;
  border-radius: var(--ks-radius-sm, 8px);
  border: 1px solid var(--ks-line);
  background: var(--ks-surface);
  color: var(--ks-ink);
  min-height: 44px;
  min-width: 0;
  width: 100%;
}

.ks-cls-days { display: flex; gap: .25rem; flex-wrap: wrap; }
.ks-cls-day {
  min-height: 44px;
  min-width: 44px;
  padding: .4rem .55rem;
  border-radius: var(--ks-radius-sm, 8px);
  border: 1px solid var(--ks-line);
  background: var(--ks-surface);
  color: var(--ks-ink-2);
  font: inherit;
  font-size: var(--ks-body-sm);
  font-weight: 650;
  cursor: pointer;
}
/* Same argument as the register's selected mark: which days a class runs is the
   fact this control exists to state, and it must be readable at a glance. */
.ks-cls-day.is-on { background: var(--ks-accent); border-color: var(--ks-accent); color: var(--ks-accent-ink, #fff); }
.ks-cls-day:focus-visible { outline: 2px solid var(--ks-focus, var(--ks-accent)); outline-offset: 2px; }

.ks-cls-credit { display: grid; gap: .25rem; }
.ks-cls-credit-label { font-weight: 650; }
.ks-cls-credit input { width: 20px; height: 20px; }
.ks-cls-credit > input, .ks-cls-credit > label { display: inline-block; vertical-align: middle; }

.ks-cls-exceptions { display: grid; gap: .5rem; }
.ks-cls-exception-list { display: grid; gap: .35rem; }
.ks-cls-exception {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr) auto;
  align-items: center;
  gap: .5rem;
}
.ks-cls-exception-date { font-weight: 650; font-variant-numeric: tabular-nums; }
.ks-cls-exception-add { display: flex; gap: .5rem; flex-wrap: wrap; align-items: center; }
.ks-cls-exception-add .ks-cls-input { flex: 0 1 12rem; }

.ks-cls-form-actions { display: flex; gap: .5rem; flex-wrap: wrap; }
.ks-cls-empty { margin: 0; }
.ks-cls-empty-state { display: grid; gap: .3rem; padding: .5rem 0; }
.ks-cls-empty-title { margin: 0; font-weight: 700; }
.ks-cls-status { margin: .5rem 0 0; }
.ks-cls-status.is-error { color: var(--ks-danger, var(--ks-ink)); font-weight: 650; }

@media (max-width: 560px) {
  .ks-cls-exception { grid-template-columns: minmax(0, 1fr); }
}

/* ── the kiosk's dojo-side controls (Admin › Settings › Check-in kiosk) ─── */

.ks-kiosk-block { display: grid; gap: .6rem; margin-bottom: 1.25rem; justify-items: start; }
.ks-kiosk-sub { margin: 0; font-size: var(--ks-body); font-weight: 700; }

.ks-kiosk-devices { display: grid; gap: .5rem; width: 100%; }
.ks-kiosk-device {
  display: grid;
  gap: .25rem;
  justify-items: start;
  padding: .7rem .9rem;
  border-radius: var(--ks-radius-sm, 8px);
  border: 1px solid var(--ks-line);
}
.ks-kiosk-device-head { display: flex; align-items: center; gap: .5rem; flex-wrap: wrap; }
.ks-kiosk-device-name { font-weight: 650; }
.ks-kiosk-device-meta { margin: 0; font-size: var(--ks-body-sm); }
.ks-kiosk-chip { flex-shrink: 0; }

/* SHOWN ONCE, so it has to be unmissable. A panel rather than a toast: a toast
   that vanishes while somebody is carrying an iPad across the room is how a
   dojo loses its enrolment code. */
.ks-kiosk-reveal {
  display: grid;
  gap: .5rem;
  justify-items: start;
  width: 100%;
  padding: 1rem 1.15rem;
  margin-bottom: 1rem;
  border-radius: var(--ks-radius-sm, 8px);
  border: 2px solid var(--ks-accent);
  background: var(--ks-canvas-warm);
}
.ks-kiosk-reveal-title { margin: 0; font-size: var(--ks-body-sm); font-weight: 700; text-transform: uppercase; letter-spacing: .06em; }
.ks-kiosk-code {
  margin: 0;
  font-family: var(--ks-font-mono, ui-monospace, SFMono-Regular, Menlo, monospace);
  font-size: clamp(1.6rem, 5vw, 2.4rem);
  font-weight: 700;
  letter-spacing: .3em;
  /* Selectable, unlike everything on the kiosk itself: this one a person copies. */
  -webkit-user-select: all;
  user-select: all;
  overflow-wrap: anywhere;
}
.ks-kiosk-reveal p { margin: 0; }

.ks-kiosk-search {
  font-size: max(1rem, var(--ks-body-sm));
  padding: .5rem .65rem;
  border-radius: var(--ks-radius-sm, 8px);
  border: 1px solid var(--ks-line);
  background: var(--ks-surface);
  color: var(--ks-ink);
  min-height: 44px;
  width: min(100%, 20rem);
}

.ks-kiosk-pins { display: grid; gap: .35rem; width: 100%; }
.ks-kiosk-pin-row {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  align-items: center;
  gap: .5rem;
  padding: .5rem .65rem;
  border-radius: var(--ks-radius-sm, 8px);
  border: 1px solid var(--ks-line);
}
.ks-kiosk-pin-who { display: grid; gap: .1rem; min-width: 0; }
.ks-kiosk-pin-name { font-weight: 650; overflow-wrap: anywhere; }
.ks-kiosk-pin-state { margin: 0; }
.ks-kiosk-pin-actions { display: flex; gap: .35rem; flex-wrap: wrap; }

.ks-kiosk-empty { display: grid; gap: .25rem; }
.ks-kiosk-empty-title { margin: 0; font-weight: 700; }
.ks-kiosk-empty p { margin: 0; }
.ks-kiosk-status { margin: .5rem 0 0; }
.ks-kiosk-status.is-error { color: var(--ks-danger, var(--ks-ink)); font-weight: 650; }

@media (max-width: 560px) {
  .ks-kiosk-pin-row { grid-template-columns: minmax(0, 1fr); }
}

/* The attendance line on My Dashboard's Working toward card. Layout only: the
   type role and the colour are the shared component layer's, because this is
   one sentence among the card's other meta lines and must not shout over the
   belt it names. */
.ks-prog-attendance { margin: .15rem 0 .35rem; }


/* ══════════════════════════════════════════════════════════════════════
   ks-dojo-payments.v1.css
   ══════════════════════════════════════════════════════════════════════ */
/* Memberships & payments — the dojo's panel, and the family's card.
 *
 * Two surfaces, one stylesheet, because they render the same three ideas: a
 * price, a state, and a number somebody is about to be charged. Keeping them
 * together is what stops the family's card and the dojo's roster drifting into
 * two vocabularies for one product.
 *
 * TOKENS ONLY. Every colour here is a design-system token, so a tenant's own
 * accent flows through without this file knowing anything about it — and so a
 * money figure never picks up a hard-coded green that fails contrast on a dark
 * band. The one place a semantic colour appears is a failed payment, which is
 * `--ks-warning` rather than the primary red: the primary red is the brand's,
 * and a family reading "your payment did not go through" must not be looking at
 * a dojo's branding.
 */

/* ── the dojo's panel ─────────────────────────────────────────────────────── */

#admin-dojo-payments { display: grid; gap: 1.25rem; min-width: 0; }

.ks-pay-subtitle {
  margin: 0 0 .5rem;
  font-size: var(--ks-body-sm, .9rem);
  font-weight: 700;
  letter-spacing: .04em;
  text-transform: uppercase;
  color: var(--ks-ink-2, #5c6472);
}

.ks-pay-connect {
  display: grid;
  gap: .5rem;
  padding: 1rem;
  border: 1px solid var(--ks-line, #e2e6ec);
  border-radius: var(--ks-radius-sm, 8px);
  background: var(--ks-surface, #fff);
}
.ks-pay-connect-title { margin: 0; font-size: var(--ks-body, 1rem); font-weight: 700; }
.ks-pay-due { margin: 0; font-weight: 600; color: var(--ks-warning, #8a5a00); }
.ks-pay-stale { font-style: italic; }
.ks-pay-fees { display: grid; gap: .25rem; margin-top: .35rem; }
.ks-pay-fee-line { margin: 0; }

/* The embedded Stripe components mount here. Height is theirs — they grow with
   their own content and cannot be sized from outside — so this only bounds the
   width and gives them room. */
.ks-pay-embedded { display: grid; gap: .75rem; min-width: 0; }

/* ── the price list ───────────────────────────────────────────────────────── */

.ks-pay-pricing { display: grid; gap: .6rem; min-width: 0; }

.ks-pay-plan {
  display: grid;
  gap: .35rem;
  padding: .75rem;
  border: 1px solid var(--ks-line, #e2e6ec);
  border-radius: var(--ks-radius-sm, 8px);
  background: var(--ks-surface, #fff);
}
/* Archived, not deleted — a membership names its plan by id, so the row stays
   readable and simply stops looking like something to sell. */
.ks-pay-plan.is-archived { opacity: .68; border-style: dashed; }

.ks-pay-plan-head {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  justify-content: space-between;
  gap: .5rem;
  min-width: 0;
}
.ks-pay-plan-name { font-weight: 700; min-width: 0; overflow-wrap: anywhere; }
.ks-pay-plan-price { font-variant-numeric: tabular-nums; font-weight: 600; }
.ks-pay-plan-actions { display: flex; flex-wrap: wrap; gap: .4rem; margin-top: .2rem; }

.ks-pay-form { display: grid; gap: .5rem; margin-top: .5rem; }
.ks-pay-field { display: grid; gap: .25rem; }
.ks-pay-field-label {
  font-size: var(--ks-body-sm, .9rem);
  font-weight: 600;
  color: var(--ks-ink-2, #5c6472);
}
.ks-pay-form-actions { display: flex; flex-wrap: wrap; gap: .4rem; }

.ks-pay-empty {
  display: grid;
  gap: .3rem;
  padding: 1rem;
  border: 1px dashed var(--ks-line, #e2e6ec);
  border-radius: var(--ks-radius-sm, 8px);
}
.ks-pay-empty-title { margin: 0; font-weight: 700; }

/* ── the takings ──────────────────────────────────────────────────────────── */

.ks-pay-earnings { display: grid; gap: .3rem; min-width: 0; }
.ks-pay-earn-row {
  display: flex;
  justify-content: space-between;
  gap: 1rem;
  padding: .25rem 0;
  min-width: 0;
}
.ks-pay-earn-label { min-width: 0; overflow-wrap: anywhere; }
/* Tabular figures so a column of money lines up. A proportional 1 next to a
   proportional 8 in a total is how a reader mis-reads their own takings. */
.ks-pay-earn-value { font-variant-numeric: tabular-nums; white-space: nowrap; }
.ks-pay-earn-row.is-total {
  border-top: 1px solid var(--ks-line, #e2e6ec);
  margin-top: .2rem;
  padding-top: .5rem;
  font-weight: 700;
}
.ks-pay-earn-pending { display: grid; gap: .2rem; margin-top: .5rem; }

/* ── who is enrolled ──────────────────────────────────────────────────────── */

.ks-pay-roster { display: grid; gap: .4rem; min-width: 0; }
.ks-pay-roster-list { display: grid; gap: .3rem; }
.ks-pay-roster-row {
  display: grid;
  /* minmax(0, …) rather than a bare fraction: `1fr` is minmax(auto, 1fr), so a
     long student name or plan title would set min-content and push the row
     past its container. The same trap the home command centre hit. */
  grid-template-columns: minmax(0, 2fr) minmax(0, 2fr) minmax(0, 1fr);
  gap: .5rem;
  padding: .35rem 0;
  border-bottom: 1px solid var(--ks-line-soft, #eef1f5);
}
.ks-pay-roster-name { font-weight: 600; overflow-wrap: anywhere; }
.ks-pay-roster-plan { color: var(--ks-ink-2, #5c6472); overflow-wrap: anywhere; }
.ks-pay-roster-state { text-align: right; }
.ks-pay-roster-state.is-warning { color: var(--ks-warning, #8a5a00); font-weight: 600; }

.ks-pay-status { margin: 0; }
.ks-pay-status.is-error { color: var(--ks-danger, #a1281f); font-weight: 600; }

/* One column below the roster's own breakpoint. Three narrow columns of
   truncated text is worse than three lines of readable text. */
@media (max-width: 560px) {
  .ks-pay-roster-row { grid-template-columns: minmax(0, 1fr); gap: .1rem; }
  .ks-pay-roster-state { text-align: left; }
}

/* ── the family's card ────────────────────────────────────────────────────── */

/* The card is the hub's (`ks-card ks-myhub-card ks-col-6`); this styles what
   goes INSIDE it. `min-width: 0` belongs on the body rather than on the card —
   on the card it let a grid item shrink below its own content, which is how a
   first version collapsed to one column on a phone. */
.ks-mem-body { display: grid; gap: .45rem; min-width: 0; padding: 0 .9rem .9rem; }
.ks-mem-mount { min-width: 0; }
.ks-mem-plan { margin: 0; font-weight: 700; }
.ks-mem-amount { margin: 0; font-variant-numeric: tabular-nums; }
.ks-mem-state { margin: 0; font-weight: 600; }
.ks-mem-state.is-warning { color: var(--ks-warning, #8a5a00); }

.ks-mem-plans { display: grid; gap: .5rem; }
.ks-mem-plan-row {
  display: grid;
  gap: .3rem;
  padding: .75rem;
  border: 1px solid var(--ks-line, #e2e6ec);
  border-radius: var(--ks-radius-sm, 8px);
}
.ks-mem-plan-head {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  justify-content: space-between;
  gap: .5rem;
  min-width: 0;
}
.ks-mem-plan-name { font-weight: 700; min-width: 0; overflow-wrap: anywhere; }
.ks-mem-plan-price { font-variant-numeric: tabular-nums; white-space: nowrap; }

/* The amount a family is about to be charged, read on OUR page before they
   reach Stripe's. It is deliberately the most prominent thing in the card. */
.ks-mem-quote {
  display: grid;
  gap: .3rem;
  padding: .75rem;
  border: 1px solid var(--ks-line, #e2e6ec);
  border-radius: var(--ks-radius-sm, 8px);
  background: var(--ks-surface-sunken, #f7f8fa);
}
.ks-mem-quote-total { margin: 0; font-weight: 700; font-variant-numeric: tabular-nums; }
.ks-mem-quote-lines { margin: 0; padding-left: 1.1rem; font-variant-numeric: tabular-nums; }

.ks-mem-error { margin: 0; color: var(--ks-danger, #a1281f); font-weight: 600; }

/* What a plan covers, on both surfaces. Quiet by design: it is orientation for
   somebody choosing, not a claim about what they may attend — nothing anywhere
   reads this field to decide whether a student can train. */
.ks-pay-plan-covers,
.ks-mem-plan-covers { margin: 0; }

/* A setup link that had already been used. Stripe's are single-use, so this is
   ordinary rather than exceptional — warning-coloured, not danger-coloured,
   because nothing has gone wrong and the fix is the button underneath it. */
.ks-pay-link-expired {
  margin: 0 0 .75rem;
  padding: .6rem .75rem;
  border-left: 3px solid var(--ks-warning);
  background: var(--ks-surface-sunken);
  border-radius: var(--ks-r-control-sm);
  font-size: var(--ks-body-sm);
  color: var(--ks-ink-2);
}


/* ══════════════════════════════════════════════════════════════════════
   ks-touch-control-sizing-responsive.v1.css
   ══════════════════════════════════════════════════════════════════════ */
/* ══════════════════════════════════════════════════════════════════════════
   SIXTEEN PIXELS, OR iOS ZOOMS THE WORLD — the platform-wide floor.

   Safari magnifies the ENTIRE PAGE when a focused text-entry control computes
   under 16px, and it does not restore on blur. The zoom outlives the control
   that caused it: it survives closing the panel, survives navigation to
   another view, and survives leaving the site. The owner tapped a field on a
   phone, tapped away, and every screen after that was rendered at roughly
   twice size with the layout running off the right edge — a roster, an
   invitation form and a match list all reported as "big blocks" that were in
   fact a correct layout seen through a stuck zoom.

   This was fixed once, correctly, for one input — the AI Sensei composer
   carries the comment and the media query to prove it. That is the lesson
   this file exists to generalise: a floor kept per-control is not a floor.
   Measured 2026-08-28 across the tenant shell on a touch viewport, the AI
   composer was the ONLY control above the line; the admin global search
   (12.48px), both upload sort selects (11.2px), three form selects (10.4px),
   the video-moment time field (13.6px) and the search palette input (15.68px)
   were all below it and every one of them would trigger the zoom.

   ── HOW IT IS WRITTEN, AND WHY ────────────────────────────────────────────

   `max(16px, var(--ks-control-font, 16px))` rather than a flat 16px, so the
   floor raises without capping: a surface that wants a larger control sets
   --ks-control-font on itself or any ancestor and keeps it. The default makes
   the expression resolve to exactly 16px everywhere else. Nothing is being
   flattened today — the same census found no control in the shell above 16px.

   `!important` deliberately, and it is the narrow case where that is right.
   This is an accessibility floor, not a style preference, and the rules it
   has to beat include id-carrying ones (#admin-global-search) that no
   selector weight available to a shared layer can outrank without becoming
   an arms race. A component that genuinely needs a different size has the
   variable above, which is a supported way to disagree rather than a race.

   `:is()` around the whole list, and this is load-bearing rather than
   tidiness. !important only decides against other !important rules by
   SPECIFICITY, and this layer competes with several — the sharpest being
   `.admin-filter-bar select, .admin-filter-bar input[type="text"]
   { font-size: .76rem !important }` in dojo-admin-light-theme-override.
   Written as a plain comma list, the input branch carried the weight of its
   six :not() attribute tests and won, while the bare `select` branch lost
   0-1-1 to 0-0-1 — so on the Members tab the search field obeyed the floor
   and the three filter selects beside it did not, which is precisely the
   half-fixed state this file exists to make impossible. :is() takes the
   specificity of its MOST specific argument and gives it to every branch, so
   select and textarea inherit the input branch's weight and the floor holds
   as one rule instead of three unequal ones.

   `pointer: coarse` matches the existing composer fix rather than
   `any-pointer`, so a touchscreen laptop driven by its trackpad keeps the
   compact desktop field. The zoom is a behaviour of the touch pointer being
   the primary one.

   NOT covered, deliberately: checkbox, radio, file, range and colour inputs
   summon no keyboard and Safari does not zoom for them, so their sizing stays
   the component's business.

   The contract is enforced by tests/design/ios-input-zoom.spec.ts, which
   measures rendered surfaces rather than reading stylesheets — font-size is a
   cascade result, and the rule that loses is invisible to a grep.
   ══════════════════════════════════════════════════════════════════════════ */

@media (pointer: coarse) {

  :is(
    input:not([type='checkbox']):not([type='radio']):not([type='file']):not([type='range']):not([type='color']):not([type='hidden']),
    select,
    textarea
  ) {
    font-size: max(16px, var(--ks-control-font, 16px)) !important;
  }
}
