/* docs/assets/app.css — ported from the approved league-intel.html mockup's
   phone-app design system (Third Coast broadcast board's own tokens/fonts,
   reused so this reads as the same product's web-shaped sibling), plus a
   real page shell, since this is a real site now, not a phone-mockup
   gallery. Mockup-only components that no page here uses have been
   dropped; what's left is only what the six pages actually render. */
*{box-sizing:border-box}
/* overscroll-behavior:none kills the document's rubber-band gesture.
   The document is not scrollable at all here -- .screen is position:fixed,
   so the body element is literally 0px tall -- but iOS grants a
   pull-to-refresh/bounce gesture to a non-scrollable document anyway, and
   once WebKit binds a touch sequence to that scroller, the app's own
   scrolling pane stops responding until the gesture and its momentum
   settle. That was the "pull down and scrolling locks for a few seconds"
   bug. Set on both html and body: the root's value is what propagates to
   the viewport, and body is stated too so the intent is not implicit. */
html,body{margin:0;overscroll-behavior:none}
body{background:#101A2C}

/* The page shell every page shares: a fixed-height column (topbar, one
   scrolling body, tab bar) capped at phone width and centred on desktop.
   app-internal design system: NOT its own palette -- these are the exact
   tokens and fonts gamenight.html/board.html/playoff.html already use for
   the real broadcast board (verified against gamenight.html directly, not
   approximated), reused so this reads as the same product's phone-shaped
   sibling, not a different app that happens to show the same data.
     --app-accent  = --time  (real doc: "clock, slot pills, progress" --
                     already a general wayfinding/accent role there, not
                     just literal clocks, so reusing it for CTAs/active
                     chips here is the SAME job, not a new one)
     --app-data    = --pend  ("upcoming, league tags" -- secondary info)
     --app-win     = --won   (brand lime, settled/positive)
     --app-live    = --live  (hot pink, "being played this minute")
   --app-loss is the one genuinely new color: the broadcast board never
   needed to dwell on "lost" as its own state (--out exists but is a
   muted eliminated-grey, not an alarm color), while a player's own W/L/T
   game log needs one that reads clearly at a glance. */
/* Pinned to the real viewport edges with position:fixed rather than
   sized off height:100%/100vh/100dvh. left:50%+translateX keeps the
   centre-on-desktop behavior without body needing to be a flex
   container for it.

   History worth keeping, because it cost three wrong fixes: the tab bar
   used to sit above the bottom of the screen in the installed PWA, with
   a dark strip beneath it. Padding, then 100dvh, then this position:fixed
   were each tried on the theory that some viewport-HEIGHT unit was
   reporting wrong -- and each landed with zero visible change, which
   should have been the tell.

   The actual cause was measured off a device screenshot (iPhone 14/15
   Pro, 1179x2556 @3x, installed): viewport 852pt, .screen bottom edge at
   793pt, a 59.0pt strip of --app-bg below the tab bar. 59pt is exactly
   safe-area-inset-top on that hardware, and the tab bar's own 91pt height
   (10+28+4+14+1+34) proves safe-area-inset-BOTTOM was being applied
   correctly all along. So insets were never broken -- the viewport height
   used for bottom-anchoring was short by precisely the TOP inset.

   That is the apple-mobile-web-app-status-bar-style:black-translucent
   bug: iOS paints the web view from y=0, under the status bar, but sizes
   the containing block as though the status bar were still reserved.
   Every bottom-anchored technique resolves against that same wrong
   number, which is why all three fixes failed identically -- they were
   all on the wrong axis.

   Fixed by dropping black-translucent for content="black" on every page,
   which removes the buggy code path rather than compensating for it.
   viewport-fit=cover STAYS: it is what keeps env(safe-area-inset-bottom)
   non-zero so .tabbar's own padding still clears the home indicator. */
.screen{
  position:fixed;top:0;bottom:0;left:50%;transform:translateX(-50%);
  max-width:480px;width:100%;overflow:hidden;
  display:flex;flex-direction:column;background:var(--app-bg);
  --app-bg:#101A2C; --app-surface:#1A263C; --app-raised:#24334E; --app-line:#2B3A54;
  --app-ink:#FFFFFF; --app-dim:#8E99AB; --app-faint:#5A6478;
  --app-accent:#FFC400; --app-data:#2E9BFF; --app-live:#FF00A0; --app-on-fill:#06070A;
  --app-win:#8CC63F; --app-loss:#E5484D; --app-tie:#FFC400;
  font-family:"Barlow Condensed","Arial Narrow",sans-serif; color:var(--app-ink); font-variant-numeric:tabular-nums;
}
/* icon glyphs: flat single-color SVG marks, not color emoji -- reads as
   part of the same bold/geometric visual language as the broadcast board
   instead of borrowed phone-keyboard art. */
.icon{width:16px;height:16px;display:inline-block;vertical-align:-3px;fill:currentColor}
.icon-stroke{fill:none;stroke:currentColor;stroke-width:2.2;stroke-linecap:round;stroke-linejoin:round}
/* padding-top is max(14px, safe-area) rather than a flat value: in an
   ordinary Safari tab env(safe-area-inset-top) is 0, so this still
   resolves to the same 14px as before -- but now that the site is
   installable (apple-mobile-web-app-capable, manifest.json) and can run
   in standalone mode with no Safari chrome at all, the OS status bar
   sits directly over the page in THAT mode, and this is what clears it
   instead of letting content run underneath. */
.topbar{flex:none;padding:max(14px, env(safe-area-inset-top)) 18px 10px;display:flex;align-items:center;justify-content:space-between}
.topbar .greet{font-size:13px;color:var(--app-dim)}
.topbar .brand{font-family:"Archivo Black",Impact,sans-serif;font-size:19px;letter-spacing:.01em}
.topbar .brand em{font-style:normal;color:var(--app-accent)}
/* padding-bottom, not a margin or a spacer element: the nav island
   below is out of flow and OVERLAYS this pane, so without reserved
   clearance the last row of every page would sit under the island
   permanently, unreachable no matter how far you scroll. Tracks
   env(safe-area-inset-bottom) because the island itself is offset
   by it. 88px = the island (~64) + its float off the bottom + breathing. */
/* contain, not none: the pane keeps its own rubber-band feel at the top
   and bottom of the feed, it just stops handing the gesture UP to the
   document once it has no scroll left to consume. That chaining is
   what locked scrolling entirely on the iOS PWA. */
.body{flex:1;min-height:0;overflow-y:auto;overscroll-behavior-y:contain;
  padding:0 18px 22px}
/* ...but ONLY on the pages that actually carry the island. search.html and
   index.html have a .body and no .tabbar, and a blanket rule left ~94px of
   dead space under the last search result. :has() keeps this a pure
   stylesheet change -- no marker class threaded through nine pages. */
.screen:has(.tabbar) .body{padding-bottom:calc(88px + env(safe-area-inset-bottom))}
.body::-webkit-scrollbar{display:none}
.card{background:var(--app-surface);border:1px solid var(--app-line);border-radius:16px;padding:16px;margin-bottom:12px}
.sec-lbl{font-size:11.5px;font-weight:800;letter-spacing:.1em;text-transform:uppercase;color:var(--app-faint);
  margin:18px 2px 9px}
.sec-lbl:first-child{margin-top:2px}

/* power rankings rows */
.rank-row{display:flex;align-items:center;gap:11px;padding:12px 4px}
.rank-row + .rank-row,.row-link + .row-link .rank-row{border-top:1px solid var(--app-line)}
.rank-row .pos{font-family:"Archivo Black",Impact,sans-serif;font-size:20px;color:var(--app-faint);width:22px}
.rank-row .nm{flex:1;min-width:0}
.rank-row .nm b{display:block;font-size:18px;font-weight:700;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.rank-row .rec{font-size:18px;font-weight:700;text-align:right;width:64px}
.form-dots{display:flex;gap:2px;margin-top:3px}
.form-dots i{width:6px;height:6px;border-radius:50%;display:block}
.form-dots i.W{background:var(--app-win)} .form-dots i.L{background:var(--app-loss)} .form-dots i.D{background:var(--app-tie);opacity:.75}

/* team detail -- the avatar is a flat slab, like a court number, not a
   gradient chip: gradients aren't part of the broadcast board's own
   visual language, flat court-style fills are. */
.team-hero{display:flex;align-items:center;gap:13px;padding:4px 2px 14px}
.team-hero .avatar{width:52px;height:52px;border-radius:14px;background:var(--app-win);
  display:grid;place-items:center;font-family:"Archivo Black",Impact,sans-serif;font-size:22px;color:var(--app-on-fill);flex:none}
.team-hero h3{font-size:19px;font-weight:700;margin:0}
.team-hero .sub{font-size:12.5px;color:var(--app-dim)}
.stat-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:10px;margin-bottom:14px}
.stat-grid .box{background:var(--app-surface);border:1px solid var(--app-line);border-radius:12px;padding:11px 8px;text-align:center}
.stat-grid .box b{display:block;font-family:"Archivo Black",Impact,sans-serif;font-size:22px}
.stat-grid .box span{font-size:10.5px;color:var(--app-faint);text-transform:uppercase;letter-spacing:.06em}
.gamelog-row{display:flex;align-items:center;gap:10px;padding:9px 2px;font-size:13px}
.gamelog-row .r{flex:none;width:26px;height:26px;border-radius:8px;display:grid;place-items:center;font-size:11px;font-weight:800}
.gamelog-row .r.W{background:rgba(140,198,63,.16);color:var(--app-win)} .gamelog-row .r.L{background:rgba(229,72,77,.16);color:var(--app-loss)}
.gamelog-row .r.D{background:rgba(255,196,0,.16);color:var(--app-tie)}
.gamelog-row .opp{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--app-ink)}
.gamelog-row .sc{font-weight:700;color:var(--app-faint)}
.gamelog-row + .gamelog-row{border-top:1px solid var(--app-line)}

/* h2h */
.vsrow{display:flex;align-items:center;gap:10px;margin:6px 0 16px}
.vsteam{flex:1;text-align:center}
.vsteam .av{width:56px;height:56px;border-radius:16px;margin:0 auto 8px;display:grid;place-items:center;
  font-family:"Archivo Black",Impact,sans-serif;font-size:24px;color:var(--app-on-fill)}
.vsteam .av.a{background:var(--app-live)}
.vsteam .av.b{background:var(--app-data)}
.vsteam b{display:block;font-size:13px}
.vsteam span{font-size:11px;color:var(--app-faint)}
.vsmid{font-family:"Archivo Black",Impact,sans-serif;font-size:15px;color:var(--app-faint);flex:none}
.probbar{height:30px;border-radius:9px;overflow:hidden;display:flex;font-family:"Archivo Black",Impact,sans-serif;
  font-size:14px;margin-top:6px}
.probbar div{display:flex;align-items:center;justify-content:center;color:var(--app-on-fill)}
.probbar .a{background:var(--app-live)}
.probbar .b{background:var(--app-data)}
.h2h-meta{font-size:12px;color:var(--app-dim);text-align:center;margin-top:8px}

/* dynasty card */
.dyn-hero{text-align:center;padding:6px 0 10px}
.dyn-hero .av{width:74px;height:74px;border-radius:50%;margin:0 auto 12px;
  background:conic-gradient(from 200deg,var(--app-win),var(--app-data),var(--app-win));display:grid;place-items:center}
.dyn-hero .av b{width:64px;height:64px;border-radius:50%;background:var(--app-bg);display:grid;place-items:center;
  font-family:"Archivo Black",Impact,sans-serif;font-size:24px}
.dyn-hero h3{font-size:21px}
.dyn-hero .sub{font-size:12.5px;color:var(--app-dim);margin-top:2px}
.badge-row{display:flex;justify-content:center;gap:8px;margin-top:12px;flex-wrap:wrap}
.badge{font-size:11px;font-weight:700;padding:5px 10px;border-radius:999px;background:var(--app-surface);
  border:1px solid var(--app-line);color:var(--app-dim)}
.partner-row{display:flex;justify-content:space-between;align-items:center;padding:9px 2px;font-size:13px}
.partner-row + .partner-row{border-top:1px solid var(--app-line)}
.partner-row .icon,.badge .icon{width:12px;height:12px;vertical-align:-1px;margin-right:2px;color:var(--app-faint)}
.badge .icon{color:var(--app-accent)}

/* playoff odds */
.odds-track{flex:1;height:8px;border-radius:5px;background:var(--app-line);position:relative;overflow:hidden;min-width:60px}
.odds-track b{position:absolute;inset:0;border-radius:5px;background:var(--app-win)}
.odds-pct{width:38px;text-align:right;font-size:12.5px;font-weight:700;font-family:"Archivo Black",Impact,sans-serif}
/* was padding:12px 8px 22px -- the 22px bottom, like .topbar's old 46px
   top, was home-indicator clearance ported from a full-screen native-app
   mockup, but a flat value with no real notch to clear just read as
   uncomfortably tall. Now that the site is installable and can run
   standalone (see .topbar's own comment), the bottom is
   max(10px, safe-area) instead of flat 10px -- 10px in an ordinary
   Safari tab (env() is 0 there), growing to actually clear the home
   indicator once there's no Safari chrome underneath it. */
/* The bottom nav is a floating glass island, not a bar welded to the
   screen edge: inset on all four sides, fully rounded, with the body
   scrolling visibly underneath it.

   position:absolute, NOT fixed. .screen is the max-480px phone column and
   is itself position:fixed, so it is the containing block; a fixed island
   would stretch to the browser window on desktop and detach from the app
   frame it belongs to.

   The safe-area inset moves out of padding and into the bottom OFFSET.
   A bar welded to the edge pads itself around the home indicator; a
   detached island floats above it instead, which is the entire point of
   detaching it. Note this is a DELIBERATE gap, unlike the accidental
   59pt one fixed earlier the same day (see the .screen comment) -- the
   rounding, the side insets and the shadow are what distinguish them. */
.tabbar{
  position:absolute;left:12px;right:12px;
  /* Sits INSIDE the home-indicator inset rather than floating clear above
     it: clearing the whole 34pt put the island visibly too high up the
     screen. Minus 14 keeps the touch targets off the indicator itself
     while dropping the island ~24pt. max() covers devices with no inset. */
  bottom:max(8px, calc(env(safe-area-inset-bottom) - 14px));
  display:flex;padding:14px 8px;
  /* Fully rounded, the way Apple's own Liquid Glass bars are. */
  border-radius:999px;
  /* The thing two earlier passes got wrong: this is tinted with WHITE,
     not with a darker navy. A dark tint is a scrim -- it subtracts light,
     and no amount of lowering its alpha makes it read as glass. Liquid
     Glass is a light-TRANSMITTING lens: it lifts and brightens what is
     behind it. Hence the white fill plus brightness() below, which is
     what turns "frosted panel" into "pane of glass".

     Three layers, matching how Apple describes the material:
       illumination -- the white fill + brightness/saturate lift
       highlight    -- the specular rim: bright inset line along the top
                       edge, fainter along the bottom, plus the gradient
       shadow       -- the outer drop shadow that separates it from the
                       content sliding underneath. */
  /* CLEAR glossy glass, not frosted. Those are opposite mixes and the
     previous pass had them backwards: "frosted" is what a heavy blur
     looks like, and "coloured" is what a big brightness lift does to a
     navy backdrop. So blur drops 24 -> 4 and brightness 1.32 -> 1.10,
     and the material's presence is carried by the SPECULAR layer
     instead -- the gloss sheen across the top, the bright rim, and the
     faint inner illumination. That is what reads as a polished lens
     rather than a pane of etched glass.

     Fill drops .10 -> .045: with this little blur the tint is doing
     almost nothing but veiling, and veiling is the thing being removed. */
  background:
    linear-gradient(to bottom,
      rgba(255,255,255,.30) 0%,
      rgba(255,255,255,.11) 16%,
      rgba(255,255,255,.015) 52%,
      rgba(255,255,255,.09) 100%),
    rgba(255,255,255,.045);
  -webkit-backdrop-filter:blur(4px) saturate(200%) brightness(1.10);
  backdrop-filter:blur(4px) saturate(200%) brightness(1.10);
  border:1px solid rgba(255,255,255,.30);
  box-shadow:
    0 14px 36px rgba(0,0,0,.40),
    inset 0 1.5px 0 rgba(255,255,255,.62),
    inset 0 -1px 0 rgba(255,255,255,.16),
    inset 0 0 24px rgba(255,255,255,.05);
}
/* Real refraction -- the background visibly bending through the rim --
   needs an SVG feDisplacementMap fed into backdrop-filter, and Safari
   restricts backdrop-filter to built-in filter functions. It is Chrome
   only, so it is deliberately NOT used here: on the iOS PWA this ships
   to, it would silently do nothing. */
/* Without blur support a 62%-opaque fill is not "glass", it is a washed
   out smear over whatever it happens to be covering. Go near-opaque. */
@supports not ((backdrop-filter:blur(1px)) or (-webkit-backdrop-filter:blur(1px))){
  .tabbar{background:rgba(42,56,84,.97)}
}
/* The tab is the positioning context for the ball behind it. */
.tab{position:relative}
/* Icon-only now -- the labels are gone from the markup, replaced by
   aria-label/title so the tabs keep an accessible name. `color` still
   matters: the glyphs are `fill:currentColor`, so this IS the icon
   colour. White at 70% rather than --app-faint, because the glass lifts
   its own backdrop and a faint slate glyph sat at nearly the same
   luminance as the pane behind it. flex:1 keeps each touch target a
   full third of the island wide even though the glyph is small. */
.tab{flex:1;text-align:center;color:rgba(255,255,255,.86)}
.tab.on{color:var(--app-win)}
/* was 36x36 -- the icon glyph itself (.icon, 23x23) is untouched, this
   only shrinks the invisible box AROUND it (36px box around a 23px icon
   was 6.5px of empty space on every side, most of what was reading as
   "too tall" once .tabbar's own padding was already cut). */
/* Just a centring box for the glyph now. The volleyball moved off the
   dot and onto the tab itself, so that it can enclose the label too. */
.tab .dot{position:relative;width:34px;height:34px;margin:0 auto;
  display:grid;place-items:center}
/* z-index keeps the glyph above the seams; 21px (from 23) buys the ring
   and the seam arcs room to read without growing the dot itself --
   the dot was deliberately cut 36->28 earlier and should stay there. */
/* The drop-shadow is not decoration: with the pane this clear, a glyph
   can end up sitting over a bright magenta odds bar or a painted court
   slab, and a flat white mark disappears into it. This is what keeps
   the icons legible against whatever happens to scroll underneath. */
.tab .dot .icon{width:27px;height:27px;vertical-align:0;position:relative;z-index:1;
  filter:drop-shadow(0 1px 2px rgba(0,0,0,.5))}
/* The ball sits behind the glyph. Three seams at 120 degrees, not two at
   180: two opposed arcs is precisely a baseball's seam pattern, which is
   what the first version read as. A volleyball's panels are a three-way
   pinwheel, so one curve is drawn three times about the centre.

   It briefly had to enclose a text label too; with the labels gone it
   tightens back to a ring around the icon.
   Drawn as an inline SVG so the seams stay crisp at any diameter and need
   no clipping tricks -- an earlier version faked them with oversized
   ellipses plus overflow:hidden, which could not survive resizing.

   z-index:-1 puts it behind the tab's own content but still in front of
   .tabbar's background: backdrop-filter makes .tabbar a stacking context,
   which is what stops a negative index escaping the island entirely. */
.tab.on::before{
  content:"";position:absolute;left:50%;top:50%;
  transform:translate(-50%,-50%);
  width:46px;height:46px;z-index:-1;
  background:url("data:image/svg+xml,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%20viewBox='0%200%2064%2064'%3E%3Ccircle%20cx='32'%20cy='32'%20r='30.4'%20fill='%238CC63F'%20fill-opacity='.16'%20stroke='%238CC63F'%20stroke-opacity='.52'%20stroke-width='1.4'/%3E%3Cg%20fill='none'%20stroke='%238CC63F'%20stroke-opacity='.34'%20stroke-width='1.4'%3E%3Cpath%20d='M13.2%2010c10.5%2012.8%2010.5%2031.2%200%2044'/%3E%3Cpath%20d='M13.2%2010c10.5%2012.8%2010.5%2031.2%200%2044'%20transform='rotate%28120%2032%2032%29'/%3E%3Cpath%20d='M13.2%2010c10.5%2012.8%2010.5%2031.2%200%2044'%20transform='rotate%28240%2032%2032%29'/%3E%3C/g%3E%3C/svg%3E") center/contain no-repeat;
}
/* a tab whose destination doesn't exist in this context (no team saved
   yet, a team with no roster) is visibly inert rather than silently dead */
.tab.off{opacity:.32}
.tab:not(.on):not(.off){cursor:pointer}

/* drill-down rows: rankings -> team page, roster -> player card */
.row-link{display:block;width:100%;text-align:left;background:none;border:0;padding:0;
  color:inherit;font:inherit;cursor:pointer;text-decoration:none}
.row-link:focus-visible{outline:2px solid var(--app-data);outline-offset:-2px;border-radius:10px}
.textlink{font-size:12px;font-weight:700;color:var(--app-data);text-decoration:none;white-space:nowrap}
.roster-row{display:flex;align-items:center;gap:10px;padding:10px 2px;font-size:13px}
.roster-row + .roster-row,.row-link + .row-link .roster-row{border-top:1px solid var(--app-line)}
.roster-row .ini{flex:none;width:26px;height:26px;border-radius:8px;background:var(--app-raised);
  display:grid;place-items:center;font-size:11px;font-weight:800;color:var(--app-ink)}
.roster-row .who{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.roster-row .cap{font-size:10.5px;font-weight:800;letter-spacing:.06em;text-transform:uppercase;color:var(--app-accent)}

/* schedule.html's own row shape -- time+court, opponent, record. No
   existing row class fits (gamelog-row assumes a played result badge,
   roster-row assumes a person not a team), so this is new rather than
   force-fit into either. */
.sched-row{display:flex;align-items:center;gap:12px;padding:11px 6px}
.sched-row + .sched-row,.row-link + .row-link .sched-row{border-top:1px solid var(--app-line)}

/* Add to Calendar (schedule.html). A button that reveals two links, not
   an overlay: with identity already known there are exactly two things
   to show, and a full-screen sheet is the wrong weight for two links. */
.cal-btn{display:flex;align-items:center;justify-content:center;gap:8px;width:100%;
  margin:2px 0 12px;padding:13px;border:1px solid var(--app-line);border-radius:12px;
  background:var(--app-surface);color:var(--app-ink);
  font:700 14px "Barlow Condensed",sans-serif;letter-spacing:.06em;text-transform:uppercase;
  cursor:pointer}
.cal-btn .icon{width:17px;height:17px;vertical-align:0}
.cal-btn:active{background:var(--app-raised)}
.cal-act{display:block;padding:12px 2px;color:var(--app-ink);text-decoration:none}
.cal-act + .cal-act{border-top:1px solid var(--app-line)}
.cal-act b{display:block;font-size:15px;color:var(--app-data)}
.cal-act span{display:block;font-size:12.5px;color:var(--app-dim);margin-top:2px}
.cal-hint{font-size:12.5px;color:var(--app-faint);margin:2px 2px 12px}
.cal-hint a{color:var(--app-dim)}
/* the same per-court paint the match card's .slab uses (--ct/--ct-ink,
   set inline from schedule.html's own copy of the COURT table), just as
   a small flat badge instead of a full-height slab -- there's no card
   edge here for a slab to run down. Same reasoning as the match card's
   retired .ctname text label: once the number is painted in the court's
   own color, spelling out "Court 7" in text next to it is redundant. */
.sched-court-badge{flex:none;width:28px;height:28px;border-radius:8px;display:grid;place-items:center;
  font-family:"Archivo Black",Impact,sans-serif;font-size:14px;background:var(--ct);color:var(--ct-ink)}
.sched-court-badge.faint{box-shadow:inset 0 0 0 1px rgba(217,201,163,.45)}
.sched-when{flex:none;width:44px;font-size:13px;font-weight:700;color:var(--app-dim)}
.sched-opp{flex:1;min-width:0;font-size:15px;font-weight:700;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.sched-opp-name{white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.sched-opp-league{font-size:11.5px;font-weight:700;color:var(--app-faint);
  white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.sched-rec{flex:none;font-size:13px;font-weight:700;color:var(--app-dim)}

/* entry-point search (index.html) */
.search-head{flex:none;padding:52px 18px 6px}
.search-head h1{font-family:"Archivo Black",Impact,sans-serif;font-size:26px;margin:0 0 6px}
.search-head p{color:var(--app-dim);font-size:14px;margin:0 0 16px}
#q{width:100%;padding:14px;border-radius:12px;border:1px solid var(--app-line);
  background:var(--app-surface);color:var(--app-ink);font-size:16px;font-family:inherit}
.result{display:block;width:100%;text-align:left;padding:14px;margin-top:10px;border-radius:12px;
  background:var(--app-surface);border:1px solid var(--app-line);color:var(--app-ink);cursor:pointer;font-size:14px}
.result b{display:block}
.result span{font-size:12px;color:var(--app-faint)}
.empty{color:var(--app-faint);font-size:13px;margin-top:16px}

/* The player picker (search.html). Close kin to .result, but a row rather
   than a card: a roster is 8-12 entries and has to fit one screen. */
.pick-row{display:flex;align-items:center;gap:11px;width:100%;text-align:left;
  padding:11px 6px;border:0;background:none;color:var(--app-ink);cursor:pointer}
.pick-row + .pick-row{border-top:1px solid var(--app-line)}
.pick-row:active{background:var(--app-raised)}
.pick-meta{flex:1;min-width:0}
.pick-name{display:block;font-size:16px;font-weight:700;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.pick-sub{display:block;font-size:12.5px;color:var(--app-dim);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.pick-cap{font-style:normal;font-size:10px;font-weight:800;letter-spacing:.08em;text-transform:uppercase;
  color:var(--app-faint);margin-left:6px}

/* ── the match card ──────────────────────────────────────────────────
   A real port of the broadcast board's own court card, not a new
   component: the rules below descend from the ones in the brackets
   repo's prototype/gamenight.html (.card/.slab/.cnum/.teams/.side/
   .tname/.capn/.rule/.vs/.statetag), with the same structure -- a
   full-height painted slab carrying the court number, and the two teams
   stacked either side of a "vs" rule with a corner state tag. (.lgtag,
   the board's league-name tag, was ported at first but removed by
   Fix 2: it truncated a real long program name to a meaningless
   fragment, and the page's own topbar already shows the full program
   name, so it was redundant duplication as well as broken. .tophat/
   .ctname -- the court's paint name spelled out as text, e.g. "purple",
   "pink" -- was removed the same way on user review: the slab's own
   paint colour already says which court this is, so the word was
   redundant too. The name isn't gone from the DOM, just from the
   render -- .slab carries it as data-paint, which is what the
   court-identity regression test in docs/tests/run.js reads now.)

   Scaled, not redrawn -- but not by one flat factor, and not by a
   single small-text floor either any more. The board is read across a
   room off a 1920x1080 TV; this is read at arm's length on a ~390px
   phone, and the two classes of element have taken different paths
   getting there:

   Large geometric elements (the slab, the court number, the corner cut)
   stayed at the original ~39% board-to-phone scale-down, and every
   proportion among THEM still matches the board:
       slab width   96px -> 38px        court number  54px -> 21px
                                         corner cut    16px ->  7px
                                         vs rule        2px ->  1px
   Team names, the corner date/time tag, the odds strip and the opponent
   roster used to be scaled down with everything else too (down to a
   ~9px "legibility floor" for the smallest of them), but user review
   called that floor out directly: team names in particular read as
   "very, very small" rather than merely compact. Those elements are now
   sized against this app's OWN "comfortable primary text" scale instead
   of the board's ratios -- e.g. .team-hero h3 (19px), .rank-row .nm b
   (14px) -- not shrunk to fit a proportion:
       .tname   12.5px -> 20px      .statetag   9.5px -> 13px
       .capn       9px -> 13px      .modds .probbar   20px height, 15px text
   .tname keeps its overflow:hidden/ellipsis -- a long real team name
   still clips rather than breaking the card's height or width, which is
   an accepted tradeoff (most real names fit at this size; a handful of
   long outliers lose their last few characters), not a bug.
   Colours are the same colours under this project's own names, which is
   the whole point of app.css's token block:
       --pend -> --app-data     --won  -> --app-win     --live -> --app-live
       --dim  -> --app-dim      --line -> --app-line    --surface -> --app-surface
       --on-fill -> --app-on-fill
   The per-court paint stays where the board puts it: --ct / --ct-ink set
   inline per card from the COURT table (index.html carries a verbatim
   copy of prototype/board-data.js's twelve real courts).

   Two class names are renamed because the page shell already owns them:
   the board's .card -> .mcard and its .body -> .mbody. Everything else
   keeps the board's own name, scoped under .mgame.

   The angular corner cuts sit on .mgame, the whole card + odds + roster
   group, rather than on .mcard alone. On the board a card is a lone
   object and the cuts frame it; here the odds strip and the opponent
   roster belong to that specific game, so the group is the object and
   one shared background plus one pair of cuts is what makes them read as
   attached rather than as three stacked sections.

   The board's Live and Final corner-tag variants are deliberately not
   ported. docs/data/schedule/*.json only ever carries state:'scheduled'
   games (archive/schedule.js drops everything else), so the tag here is
   always the board's upcoming-game one -- which already showed a start
   time like "9:30". It shows the real date and start time instead. */
.mgame{
  --mc-slabw:38px; --mc-cut:7px;
  position:relative;background:var(--app-surface);margin-bottom:14px;
  clip-path:polygon(0 0, calc(100% - var(--mc-cut)) 0, 100% var(--mc-cut),
                    100% 100%, var(--mc-cut) 100%, 0 calc(100% - var(--mc-cut)));
}
.mcard{position:relative;padding:8px 11px 9px calc(var(--mc-slabw) + 11px);display:flex;align-items:center}
/* the slab IS the left edge -- one object, not a bar plus a chip */
.mgame .slab{position:absolute;left:0;top:0;bottom:0;width:var(--mc-slabw);z-index:1;
  background:var(--ct);display:grid;place-items:center}
/* black, maroon and dark green sit under 3:1 against this ground; the
   board rings them in sand so they read as a chip, not a hole. 1px here
   rather than the board's 2px -- same ring at phone scale. */
.mgame .mcard.faint .slab{box-shadow:inset 0 0 0 1px rgba(217,201,163,.45)}
.mgame .cnum{font-family:"Archivo Black",Impact,sans-serif;font-size:21px;line-height:1;color:var(--ct-ink)}
/* padding-top pushes ALL content below the corner badge instead of
   reserving horizontal space from just the top team row. The badge
   (.statetag, position:absolute, top:0 relative to .mcard) measures
   25px tall via getBoundingClientRect() -- constant regardless of its
   text length, since height comes from font-size/line-height/padding,
   not content width. .mcard's own 8px top padding already accounts
   for 8 of those 25px before .mbody even starts, so .mbody needs
   25px of its own padding-top to clear the remaining 17px plus an
   8px visual gap (131 -> 156 in real rendered coordinates, vs. the
   badge's own bottom edge at 148). This replaces the old approach of
   reserving horizontal space on .side.a alone (the top team row): that
   made the TOP row (always the PLAYER'S OWN team) meaningfully
   narrower than the bottom row, every card, every load -- a real
   systematic bias against exactly the name a player most wants to
   read clearly. Pushing both rows below the badge instead costs the
   card some extra height (roughly a badge's worth) but gives BOTH
   rows the card's full ~294px content width, equally -- the badge now
   occupies the same empty top band the old .tophat row used to. */
.mgame .mbody{position:relative;z-index:1;flex:1;min-width:0;display:flex;flex-direction:column;gap:4px;padding-top:25px}
.mgame .teams{position:relative}
.mgame .side{display:flex;align-items:baseline;gap:5px;min-width:0;padding:1px 0}
/* Fix 1: the odds strip below colours "my team" pink (.probbar .a) and
   the opponent blue (.probbar .b) with no visual anchor tying either
   colour back to a name up here -- matchup.html has this anchor (its
   pink/blue team-initial avatars) but there's no room for a full avatar
   at this card's compact scale, so a cheap dot stands in for one: same
   two colours, same top-is-mine/bottom-is-opponent order, so a player
   can match the percentage to the name at a glance without reading. */
.mgame .side.a::before,.mgame .side.b::before{content:'';flex:none;align-self:center;
  width:6px;height:6px;border-radius:50%}
.mgame .side.a::before{background:var(--app-live)}
.mgame .side.b::before{background:var(--app-data)}
/* min-width:0 is the one addition the board's rule doesn't need: a
   flex item won't shrink below its content without it, so a long real
   team name would push out of the card instead of ellipsing. */
.mgame .tname{display:inline-block;min-width:0;text-transform:uppercase;font-weight:700;font-size:20px;letter-spacing:.005em;
  line-height:1.15;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;
  color:inherit;text-decoration:none}
.mgame .capn{flex:none;font-size:13px;letter-spacing:.04em;text-transform:uppercase;
  color:var(--app-dim);white-space:nowrap}
.mgame .rule{position:relative;height:1px;margin:3px 0;background:var(--app-line)}
/* --app-dim, not a darker grey: the board's note here is that #5B6577
   only manages 3.14:1 against this surface. right:0, not the board's
   right:4px: that inset leaves a sliver of rule showing past the label,
   which is a deliberate detail at 1920 and an apparent rendering glitch
   once it scales to under 2px. */
.mgame .vs{position:absolute;right:0;top:50%;transform:translateY(-50%);
  background:var(--app-surface);color:var(--app-dim);
  font-size:8px;font-weight:600;letter-spacing:.16em;text-transform:uppercase;padding:0 5px;line-height:1.4}
/* state -- the corner tag, and nothing else. Always the board's
   upcoming-game blue here (see the note above). */
.mgame .statetag{position:absolute;right:var(--mc-cut);top:0;z-index:3;
  background:var(--app-data);color:var(--app-on-fill);
  font-size:13px;font-weight:700;letter-spacing:.06em;text-transform:uppercase;padding:4px 9px 5px}

/* Head-to-head history for THIS matchup specifically -- one shared row,
   not one per team (Eric: "if the match is between team A and team B,
   there is only 1 row of history needed for A vs B matches"), between the
   card and the odds strip. No pink/blue identity dot here: the row is
   already anchored to "my team's" perspective the same way the rest of
   the card is (top slot, ratings, etc.), so there is no second team to
   distinguish within it -- the W/L/D dots keep team.html's own
   win/loss/tie colors (.form-dots i.W/.L/.D) unambiguously instead.
   .flbl reuses .mroster .rlbl's own faint/uppercase/small treatment so
   the two labels in this card read as the same kind of thing. Dots sized
   8x8, up from team.html's 6x6: at this row's much smaller overall
   footprint (no game log, no roster alongside it) 6px read as flyspecks
   in rendered review; 8px was the first size that read clearly as a row
   of dots at a glance rather than needing a squint. */
.mgame .mform{border-top:1px solid var(--app-line);padding:9px 11px;display:flex;align-items:center;gap:8px}
.mgame .mform .flbl{flex:none;font-size:10.5px;font-weight:800;letter-spacing:.1em;text-transform:uppercase;color:var(--app-faint)}
.mgame .mform .fdots{display:flex;align-items:center;gap:4px;overflow:hidden}
.mgame .mform .fdots i{flex:none;display:block;width:8px;height:8px;border-radius:50%}
.mgame .mform .fdots i.W{background:var(--app-win)}
.mgame .mform .fdots i.L{background:var(--app-loss)}
.mgame .mform .fdots i.D{background:var(--app-tie);opacity:.75}
.mgame .mform .empty{font-size:12px;color:var(--app-faint)}

/* team.html's own head-to-head section -- same dot look as the match
   card's .mgame .mform .fdots/.empty, but this page has no .mgame
   ancestor to scope under, and bare .fdots/.empty would collide with
   other existing classes (.empty is already search.html's "no results"
   message, at a different font-size) -- so these get their own names
   instead of trying to force a shared selector across unrelated pages. */
.h2h-dots{display:flex;align-items:center;gap:5px;overflow:hidden}
.h2h-dots i{flex:none;display:block;width:9px;height:9px;border-radius:50%}
.h2h-dots i.W{background:var(--app-win)}
.h2h-dots i.L{background:var(--app-loss)}
.h2h-dots i.D{background:var(--app-tie);opacity:.75}
.h2h-empty{font-size:13px;color:var(--app-faint)}

/* team.html's "Opponents" card -- one row per team in the standings
   (every league opponent, not just the ones already met, and not one
   row per game the way the retired game-log was), sorted by power
   rank. A list this dense needs a much smaller footprint than the
   match card's own .h2h-dots/.probbar, so these are compact variants
   with their own names rather than a reuse at a different size --
   .opp-dots keeps this page's own win/loss/tie colors, .opp-bar keeps
   .probbar's pink/blue "which team" colors. Both fixed-width columns
   (.opp-dots/.opp-odds) always render, even for the "not yet met" /
   "not enough games" empty states, so every row's three columns stay
   aligned regardless of which rows have real data. */
/* two .sec-lbl side by side (same uppercase/faint/letter-spaced look
   every other section header on this page already uses) rather than a
   one-off style, so this reads as the same kind of label, just a pair --
   margin moves onto the row itself so the two children don't double it. */
.opp-header{display:flex;justify-content:space-between;align-items:baseline;margin:18px 2px 9px}
.opp-header .sec-lbl{margin:0}
.opp-row{display:flex;align-items:center;gap:10px;padding:10px 2px}
.opp-row + .opp-row,.row-link + .row-link .opp-row{border-top:1px solid var(--app-line)}
.opp-row .nm{flex:1;min-width:0;font-size:14px;font-weight:700;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;color:var(--app-ink)}
.opp-dots{flex:none;width:38px;display:flex;align-items:center;gap:4px;overflow:hidden}
.opp-dots i{flex:none;display:block;width:7px;height:7px;border-radius:50%}
.opp-dots i.W{background:var(--app-win)}
.opp-dots i.L{background:var(--app-loss)}
.opp-dots i.D{background:var(--app-tie);opacity:.75}
.opp-empty{font-size:11px;color:var(--app-faint)}
.opp-odds{flex:none;width:84px;display:flex;align-items:center;justify-content:flex-end;gap:6px}
.opp-bar{flex:1;height:8px;border-radius:5px;overflow:hidden;display:flex;background:var(--app-line)}
.opp-bar .a{background:var(--app-live)}
.opp-bar .b{background:var(--app-data)}
.opp-pct{flex:none;width:30px;text-align:right;font-size:11.5px;font-weight:700;color:var(--app-dim);font-variant-numeric:tabular-nums}

/* team.html's roster, upgraded to the same 2-column grid + larger font
   the match card's roster uses (.mgame .mroster .rgrid/.roster-row),
   generalized onto this page's own .teamroster wrapper instead of
   .mgame .mroster -- this page has no .mgame ancestor either, and its
   roster section is much wider than the match card's ~294px column, so
   these are independently-set values, not copy-pasted ones; verify by
   rendering rather than assuming the card's numbers carry over. */
.teamroster .rgrid{display:grid;grid-template-columns:1fr 1fr;column-gap:16px;row-gap:14px;margin-top:2px}
.teamroster .row-link + .row-link .roster-row{border-top:none}
.teamroster .roster-row{gap:10px;padding:0;font-size:18px}
.teamroster .roster-row .ini{width:32px;height:32px;border-radius:9px;font-size:13px}
.teamroster .rmeta{display:flex;flex-direction:column;min-width:0;gap:2px}
.teamroster .roster-row .cap{font-size:11px}

/* attached to the card above it: same background, divided by a line
   rather than a gap, inside the same pair of corner cuts. */
.mgame .modds{border-top:1px solid var(--app-line);padding:10px 11px 11px}
.mgame .modds .probbar{height:28px;border-radius:0;font-size:15px;margin-top:0}
.mgame .modds .h2h-meta{font-size:12.5px;margin-top:7px}
.mgame .mroster{border-top:1px solid var(--app-line);padding:2px 11px 8px}
.mgame .mroster .rlbl{font-size:10.5px;font-weight:800;letter-spacing:.1em;text-transform:uppercase;
  color:var(--app-faint);padding:9px 2px 0}
/* Fix 3: rosterRowsHTML() reuses team.html's .roster-row/.ini/.who/.cap
   classes (there's no build step to fork a component, this project's
   established convention), but its OWN markup now wraps players in a
   .rgrid and adds a .rmeta column -- see rosterRowsHTML()'s own comment.
   Scoped to .mgame .mroster only -- team.html's own single-column roster
   (a different, separately-generated block of markup that happens to
   share these class names) is untouched either way.
   Two columns, not a stack: real archived rosters run 2-7 players
   (median 2, p75 4, max 7 -- checked against docs/data/rosters/*.json),
   so 2 columns means the common 2-player team fills one tidy row instead
   of two lonely stacked rows, and a 4-player team fills exactly two full
   rows. An odd count (5, 7) leaves one cell alone in the last row --
   rendered review confirmed that reads fine as "an odd team size" rather
   than broken, so it is left alone rather than spanned full-width.
   Font sized up per Eric's ask (.who 14px -> 17px, one step past even the
   prior "make it bigger" pass) -- rendered review confirmed 17px still
   leaves comfortable room to ellipsis a long real first name inside a
   single ~145px column at this card's real width, not just the short
   fixture names.
   The row-divider line the stacked layout used (.row-link + .row-link
   .roster-row's border-top, defined once above for team.html) assumes
   vertically-adjacent DOM siblings are vertically-adjacent on screen.
   In a 2-column grid the second sibling sits to the RIGHT of the first,
   not below it, so that rule would draw a top border on nearly every
   cell regardless of which visual row it is in -- overridden off here,
   under the same higher-specificity .mgame .mroster scope, in favor of
   the grid's own row-gap as the only row separator. Rendered review
   confirmed that gap alone reads as clearly separated rows without a
   line; adding one back on top of it looked busier, not clearer, at
   this compact a scale. */
.mgame .mroster .rgrid{display:grid;grid-template-columns:1fr 1fr;column-gap:14px;row-gap:12px;margin-top:2px}
.mgame .mroster .row-link + .row-link .roster-row{border-top:none}
.mgame .mroster .roster-row{gap:9px;padding:0;font-size:17px}
.mgame .mroster .roster-row .ini{width:28px;height:28px;border-radius:8px;font-size:12px}
.mgame .mroster .rmeta{display:flex;flex-direction:column;min-width:0;gap:2px}
.mgame .mroster .roster-row .cap{font-size:10px}

/* ── season.html ─────────────────────────────────────────────────────
   The one primary action in the app: "That's me", which moves a
   returning player's saved pointer into the new season. Sized and
   coloured to be unmistakably the thing to tap, because every other
   route off that screen is a deliberate correction. */
.cta{display:block;width:100%;margin-top:14px;padding:15px;border:0;border-radius:12px;
  background:var(--app-win);color:var(--app-on-fill);cursor:pointer;
  font-family:"Barlow Condensed",system-ui,sans-serif;font-size:17px;font-weight:700;
  letter-spacing:.04em;text-transform:uppercase}
.cta:active{filter:brightness(.92)}
.season-team{font-family:"Archivo Black",Impact,sans-serif;font-size:22px;line-height:1.15}
.season-sub{color:var(--app-faint);font-size:12.5px;margin-top:6px}

/* ── court.html: the venue floor ─────────────────────────────────────
   A port of the broadcast board's courtmap down to a phone. The board's
   physical arrangement is kept -- north bank 8-12 left, 6-7 centre,
   south bank 1-5 right -- because matching what you are standing in is
   the entire point of a court map. What could not come with it is the
   full matchup on every card: the board has 1920px and this has ~108px
   per column. So a card is its colour chip plus league, and tapping
   opens the matchup below the floor. */

/* The slot toggle sits OUTSIDE .body, so it stays put while the floor
   scrolls -- it is the control for what the floor is showing. */
.slotbar{flex:none;display:flex;gap:6px;padding:0 18px 10px}
.slot{flex:1;position:relative;padding:7px 0 8px;border-radius:11px;cursor:pointer;
  background:var(--app-surface);border:1px solid var(--app-line);color:var(--app-dim);
  font-family:"Barlow Condensed",system-ui,sans-serif;font-size:15px;font-weight:700}
.slot.on{background:var(--app-raised);border-color:var(--app-win);color:var(--app-ink)}
/* The live slot is marked, not selected: the two are usually the same
   but come apart the moment a player taps a different slot, and the dot
   is what still says which one the venue is actually playing. */
.slot .livedot{position:absolute;top:5px;right:6px;width:6px;height:6px;border-radius:50%;
  background:var(--app-live);box-shadow:0 0 0 3px color-mix(in srgb, var(--app-live) 26%, transparent)}

.floor{display:grid;grid-template-columns:1fr 1fr 1fr;gap:9px;margin-top:2px}
.bankcol{min-width:0}
.bank-label{font-size:10px;letter-spacing:.14em;text-transform:uppercase;color:var(--app-faint);
  margin:0 0 6px 2px}
/* A FIXED row height, shared by all three banks, is what keeps the floor
   in register. 1fr plus a percentage override for the short centre bank
   was the obvious thing and it does not work: the banks have no definite
   height to resolve against, so each column sized to its own content and
   the centre drifted out of line with the two beside it. An explicit
   row height needs no such resolution and holds whatever each bank
   contains -- five courts or two. */
.floor{--court-row:76px;--court-gap:7px}
.bank{display:grid;grid-auto-rows:var(--court-row);gap:var(--court-gap)}
/* Courts 6 and 7 sit BETWEEN the banks either side of them on the real
   floor, so the center column is offset by half a row pitch rather than
   starting level with rows 1 and 2. Half the PITCH, not half the row --
   the gap has to be in it or the stagger drifts as either changes.

   padding, not margin, and that is load-bearing: a margin-top here
   collapses with .bank-label's own margin-bottom, so the two do not add.
   The offset silently came out 6px short (35.5 instead of 41.5), which
   is small enough to look deliberate and be wrong. Padding does not
   collapse, so the offset is exactly what it says. */
.bank.mid{padding-top:calc((var(--court-row) + var(--court-gap)) / 2)}

/* The floor card echoes the big match card's shape -- painted slab down
   the left, teams stacked beside it -- so the map and the card that
   opens from it read as the same object at two sizes. At ~86px of text
   width the names clamp to two lines rather than truncating to nothing;
   stripping the captain parenthetical first is what buys the room. */
.court-card{display:flex;align-items:stretch;gap:0;width:100%;min-height:76px;
  padding:0;border-radius:12px;overflow:hidden;cursor:pointer;text-align:left;
  background:var(--app-surface);border:1px solid var(--app-line);color:var(--app-ink)}
.court-card .cslab{flex:none;width:22px;display:grid;place-items:center;
  background:var(--ct);color:var(--ct-ink);
  font-family:"Archivo Black",Impact,sans-serif;font-size:13px;line-height:1}
/* maroon, dark green and black fall under 3:1 against this ground -- an
   inset sand edge keeps the slab from reading as a hole cut in the card.
   Same rule and the same three courts as the match card. */
.court-card.faint .cslab{box-shadow:inset 0 0 0 2px rgba(217,201,163,.45)}
.court-card .cteams{flex:1;min-width:0;display:flex;flex-direction:column;
  justify-content:center;gap:1px;padding:6px 7px}
.court-card .ctname{font-size:9.5px;line-height:1.16;font-weight:700;color:var(--app-ink);
  overflow:hidden;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical}
.court-card .ctname.mine{color:var(--app-win)}
.court-card .cvs{font-size:7.5px;font-weight:700;letter-spacing:.14em;text-transform:uppercase;
  color:var(--app-faint);margin:1px 0}
.court-card .ctopen{font-size:10px;color:var(--app-faint)}
/* An unused court stays on the floor as an outline. Dropping it would
   leave a hole where a real court is, which is exactly the orientation
   the map exists to give -- Wednesday only lights three of twelve. */
.court-card.open{border-style:dashed;background:transparent}
.court-card.open .cslab{opacity:.35}
.court-card.mine{border-color:var(--app-win)}
.court-card.sel{border-color:var(--app-ink);background:var(--app-raised)}

/* The floor and the card that opens from it were touching. They are two
   separate objects and need to read as such. */
.court-detail-host{display:block;margin-top:20px}
.court-detail{margin-top:0;background:var(--app-surface);border:1px solid var(--app-line);
  border-radius:16px;padding:13px 15px}
.cd-head{display:flex;align-items:center;gap:9px;font-size:11.5px;font-weight:800;
  letter-spacing:.08em;text-transform:uppercase;color:var(--app-faint)}
.cd-chip{display:grid;place-items:center;width:28px;height:28px;border-radius:9px;flex:none;
  background:var(--ct);color:var(--ct-ink);
  font-family:"Archivo Black",Impact,sans-serif;font-size:15px;line-height:1}
.cd-teams{display:flex;flex-direction:column;gap:3px;margin-top:9px}
.cd-team{font-family:"Archivo Black",Impact,sans-serif;font-size:17px;line-height:1.15;
  color:var(--app-ink);text-decoration:none;overflow:hidden;text-overflow:ellipsis}
.cd-team.mine{color:var(--app-win)}
.cd-vs{font-size:10.5px;font-weight:700;letter-spacing:.16em;text-transform:uppercase;color:var(--app-faint)}
.cd-open{margin:9px 0 0;font-size:13px;color:var(--app-faint)}
.cd-hint{margin-top:16px;font-size:12.5px;color:var(--app-faint);text-align:center}

/* ---- omnisearch -------------------------------------------------------
   The magnifier sits at the right end of every .topbar (already a
   space-between flex row, so appending it is all the placement it needs).
   The sheet covers the whole .screen -- .screen's transform makes it the
   containing block, so absolute inset:0 is exactly the app's frame even
   on a desktop where .screen is a centred 480px column. */
.omni-btn{flex:none;width:40px;height:40px;border-radius:12px;border:1px solid var(--app-line);
  background:var(--app-surface);color:var(--app-ink);display:grid;place-items:center;cursor:pointer;padding:0}
.omni-btn .icon{width:19px;height:19px;vertical-align:0}
.omni-btn:focus-visible{outline:2px solid var(--app-data);outline-offset:2px}
.omni{position:absolute;inset:0;z-index:50;background:var(--app-bg);display:flex;flex-direction:column}
.omni-head{flex:none;display:flex;align-items:center;gap:10px;
  padding:max(14px, env(safe-area-inset-top)) 16px 10px}
.omni-field{flex:1;min-width:0;display:flex;align-items:center;gap:8px;height:42px;padding:0 12px;
  border-radius:12px;background:var(--app-surface);border:1px solid var(--app-line);color:var(--app-dim)}
.omni-field:focus-within{border-color:var(--app-data)}
.omni-field .icon{flex:none;width:17px;height:17px;vertical-align:0}
/* 16px, not smaller: iOS zooms the whole page into any input under 16px. */
.omni-field input{flex:1;min-width:0;height:100%;border:0;outline:0;background:none;color:var(--app-ink);
  font:600 16px "Barlow Condensed","Arial Narrow",sans-serif;-webkit-appearance:none;appearance:none}
.omni-field input::-webkit-search-cancel-button{-webkit-appearance:none}
.omni-cancel{flex:none;border:0;background:none;color:var(--app-data);font:700 15px "Barlow Condensed",sans-serif;
  padding:8px 2px;cursor:pointer}
.omni-body{flex:1;min-height:0;overflow-y:auto;overscroll-behavior-y:contain;padding:0 16px 24px}
.omni-hint{color:var(--app-faint);font-size:14px;margin:18px 2px}
.omni-more{color:var(--app-faint);font-size:12px;margin:-4px 2px 6px}
.omni-list{padding:2px 12px}
.omni-row{display:flex;align-items:center;gap:11px;padding:10px 0;color:var(--app-ink);text-decoration:none}
.omni-row + .omni-row{border-top:1px solid var(--app-line)}
.omni-row .ini{flex:none;width:32px;height:32px;border-radius:10px;background:var(--app-raised);
  display:grid;place-items:center;font-size:12px;font-weight:800}
.omni-row .ini.team{background:var(--app-win);color:var(--app-on-fill)}
.omni-meta{flex:1;min-width:0}
.omni-meta b{display:block;font-size:16px;font-weight:700;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.omni-meta span{display:block;font-size:12.5px;color:var(--app-dim);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.omni-now{font-style:normal;font-size:10px;font-weight:800;letter-spacing:.08em;text-transform:uppercase;
  color:var(--app-win);margin-left:4px;vertical-align:2px}

/* ---- promoted / moved-down badge ---------------------------------------
   Lime up, red down: the same win/loss pair every other signal on the site
   already uses, so it reads without a legend. */
.mv{display:inline-block;font-size:10.5px;font-weight:800;letter-spacing:.05em;text-transform:uppercase;
  padding:2px 7px;border-radius:999px;white-space:nowrap;vertical-align:1px}
.mv.up{color:var(--app-win);background:rgba(140,198,63,.14)}
.mv.down{color:var(--app-loss);background:rgba(229,72,77,.14)}
.rank-row .nm .mv{margin-top:3px}
.team-hero .sub .mv{margin-left:6px}

/* ---- season history rows (team.html, player.html) ------------------- */
.hist-row{display:flex;align-items:center;gap:10px;padding:10px 0;color:var(--app-ink);text-decoration:none}
.hist-row + .hist-row,.row-link + .row-link .hist-row{border-top:1px solid var(--app-line)}
.hist-row .when{flex:none;width:62px;font-size:12px;font-weight:700;color:var(--app-faint)}
.hist-row .what{flex:1;min-width:0}
.hist-row .what b{display:block;font-size:15px;font-weight:700;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.hist-row .what > span:not(.mv){display:block;font-size:12px;color:var(--app-dim);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.hist-row .what .mv{margin-top:3px}
.hist-row .res{flex:none;text-align:right;font-size:15px;font-weight:700}
.hist-row .res span{display:block;font-size:11px;font-weight:600;color:var(--app-faint)}
.hist-row.here .what b{color:var(--app-win)}
.hist-row .cap-tag{font-size:10px;font-weight:800;letter-spacing:.06em;text-transform:uppercase;color:var(--app-accent);
  vertical-align:1px;margin-left:4px}
/* team.html's Opponents name cell is nowrap+ellipsis; the badge drops to
   its own line under the name instead of being clipped off the end. */
.opp-row .nm .nmt{display:block;overflow:hidden;text-overflow:ellipsis}
.opp-row .nm .mv{margin-top:3px}

/* ---- court.html: the bar and the entrance ------------------------------
   The open floor under courts 6-7 holds the rest of the room, so the map
   has landmarks beyond court numbers. Pinned to the bottom of the centre
   column (which the floor grid stretches to the full bank height), so it
   sits level with courts 12 and 5 however tall a card grows. The
   entrance strip comes first in the row: it is between the north bank and
   the bar, which is exactly where it is in the building. */
.bankcol.center{position:relative}
.venue{position:absolute;left:0;right:0;bottom:0;display:grid;
  grid-template-columns:16px 4px repeat(4,1fr);grid-template-rows:repeat(4,1fr)}
/* The dashed outline is one item spanning the 4x4 box and sets its
   height (square); the bar pieces and the entrance are placed on the same
   grid, so the entrance spans exactly rows 2-4, level with the upright. */
.venue-box{grid-column:3 / 7;grid-row:1 / 5;aspect-ratio:1;
  border:1px dashed var(--app-line);border-radius:12px}
.bar{background:color-mix(in srgb, var(--app-accent) 30%, var(--app-surface))}
.bar-up{grid-column:3;grid-row:2 / 4;margin-left:1px}
.bar-run{grid-column:3 / 7;grid-row:4;display:grid;place-items:center;margin:0 1px 1px;border-radius:0 0 11px 11px}
.bar-run span,.entrance span{font-size:10px;font-weight:800;letter-spacing:.14em;text-transform:uppercase}
.bar-run span{color:var(--app-accent)}
.entrance{grid-column:1;grid-row:2 / 5;display:grid;place-items:center;
  border-left:3px solid var(--app-data);border-radius:2px}
.entrance span{writing-mode:vertical-rl;transform:rotate(180deg);color:var(--app-data);white-space:nowrap}

/* ---- install guide (app.js mountInstallNag) ----------------------------
   Covers the whole .screen, above the tab bar and the omnisearch. */
.install-nag{position:absolute;inset:0;z-index:80;background:var(--app-bg);display:flex;flex-direction:column}
.nag-scroll{flex:1;min-height:0;overflow-y:auto;overscroll-behavior-y:contain;text-align:center;
  padding:max(28px, env(safe-area-inset-top)) 22px calc(28px + env(safe-area-inset-bottom))}
.nag-icon{width:72px;height:72px;border-radius:17px;box-shadow:0 6px 24px rgba(0,0,0,.45)}
.install-nag h2{font-family:"Archivo Black",Impact,sans-serif;font-size:26px;margin:14px 0 6px}
.nag-lede{font-size:15px;line-height:1.35;color:var(--app-dim);margin:0 auto 16px;max-width:320px}
.nag-install,.nag-copy{display:block;width:100%;max-width:320px;margin:0 auto;height:52px;border:0;border-radius:14px;
  background:var(--app-win);color:var(--app-on-fill);font:800 18px "Barlow Condensed",sans-serif;
  letter-spacing:.04em;text-transform:uppercase;cursor:pointer}
.nag-or{font-size:12px;color:var(--app-faint);margin:12px 0 0}
/* The one-tap button only exists when Chrome has actually offered it. */
.install-nag:not(.can-install) .nag-install,.install-nag:not(.can-install) .nag-or{display:none}
.nag-steps{list-style:none;counter-reset:step;margin:18px auto 0;padding:0;max-width:340px;text-align:left}
.nag-steps li{counter-increment:step;position:relative;padding:12px 12px 12px 50px;margin-bottom:8px;
  background:var(--app-surface);border:1px solid var(--app-line);border-radius:14px;font-size:15.5px;line-height:1.3}
.nag-steps li::before{content:counter(step);position:absolute;left:12px;top:50%;transform:translateY(-50%);
  width:26px;height:26px;border-radius:50%;background:var(--app-data);color:#fff;display:grid;place-items:center;
  font-family:"Archivo Black",Impact,sans-serif;font-size:13px}
.nag-steps small{display:block;font-size:12px;color:var(--app-faint);margin-top:2px}
.nag-steps .ig{width:19px;height:19px;vertical-align:-4px;color:var(--app-data);margin:0 1px}
.nag-note{font-size:12.5px;color:var(--app-faint);margin:12px auto 0;max-width:320px}
.nag-later{margin:18px auto 0;display:block;border:0;background:none;color:var(--app-dim);
  font:600 14px "Barlow Condensed",sans-serif;text-decoration:underline;text-underline-offset:3px;cursor:pointer;padding:8px}

/* The arrow at the real button in the browser's own chrome. */
.nag-arrow{position:absolute;z-index:1;font:900 40px/1 "Archivo Black",Impact,sans-serif;color:var(--app-accent);
  pointer-events:none;text-shadow:0 2px 12px rgba(0,0,0,.6)}
.nag-arrow.bottom-center{bottom:calc(16px + env(safe-area-inset-bottom));left:50%;margin-left:-12px;animation:nag-bob-down 1.1s ease-in-out infinite}
.nag-arrow.bottom-right{bottom:calc(16px + env(safe-area-inset-bottom));right:18px;animation:nag-bob-down 1.1s ease-in-out infinite}
.nag-arrow.top-right{top:max(6px, env(safe-area-inset-top));right:16px;animation:nag-bob-up 1.1s ease-in-out infinite}
@keyframes nag-bob-down{50%{transform:translateY(9px)}}
@keyframes nag-bob-up{50%{transform:translateY(-9px)}}

/* The looping walkthrough phone: four frames on a 9s loop -- tap Share,
   tap Add to Home Screen, tap Add, the icon on the Home Screen. */
.nag-demo{margin:18px auto 0;width:170px;height:250px;border-radius:26px;padding:8px;
  background:#05080f;border:2px solid var(--app-line);box-shadow:0 10px 30px rgba(0,0,0,.4)}
.nd-screen{position:relative;width:100%;height:100%;border-radius:19px;overflow:hidden;background:#f2f2f7;
  font:600 10px -apple-system,"Barlow Condensed",sans-serif;color:#111}
.nd-page{position:absolute;inset:14px 12px 44px;display:flex;flex-direction:column;gap:8px}
.nd-page i{display:block;height:22px;border-radius:6px;background:#dcdce3}
/* One frame shown at a time (startInstallDemo toggles .on). Each
   frame's own motion -- the sheet sliding up, the ring landing, the icon
   popping in -- is keyed to .on, so it replays every time the frame
   comes round. */
.nd-frame{position:absolute;inset:0;opacity:0;transition:opacity .25s}
.nd-frame.on{opacity:1}
.nd-bar{position:absolute;left:0;right:0;bottom:0;height:36px;background:#fff;border-top:1px solid #ddd;
  display:flex;align-items:center;justify-content:space-around;font-size:15px;color:#007aff}
/* Safari 26: a floating pill -- back, the address, then •••. */
.nd-bar.compact{left:6px;right:6px;bottom:6px;height:30px;border:1px solid #ddd;border-radius:16px;
  justify-content:space-between;padding:0 10px;box-shadow:0 2px 8px rgba(0,0,0,.12)}
.nd-url{flex:1;text-align:center;font-size:10px;color:#333}
.nd-bar .ig,.nd-row .ig{width:16px;height:16px}
.nd-share,.nd-more{display:grid;place-items:center;color:#007aff}
.nd-menu{position:absolute;right:6px;bottom:42px;width:122px;background:#fff;border-radius:12px;padding:4px;
  box-shadow:0 6px 20px rgba(0,0,0,.2);transform-origin:bottom right}
.nd-frame.on .nd-menu{animation:nd-grow .3s ease-out}
@keyframes nd-grow{from{transform:scale(.6);opacity:0}}
.nd-sheet{position:absolute;left:0;right:0;bottom:0;background:#fff;border-radius:14px 14px 0 0;padding:8px 8px 12px;
  box-shadow:0 -4px 16px rgba(0,0,0,.15)}
.nd-frame.on .nd-sheet{animation:nd-slide .35s ease-out}
@keyframes nd-slide{from{transform:translateY(100%)}}
.nd-row{display:flex;align-items:center;justify-content:space-between;padding:8px 8px;border-radius:8px;font-size:10.5px}
.nd-row + .nd-row{border-top:1px solid #eee}
.nd-row.hl{background:#e8f1ff;color:#007aff;font-weight:800}
.nd-dialog{position:absolute;inset:0;background:#f2f2f7;padding:10px}
.nd-dh{display:flex;justify-content:space-between;color:#007aff;font-size:11px;margin-bottom:14px}
.nd-app{display:flex;align-items:center;gap:8px;background:#fff;border-radius:10px;padding:8px;font-size:12px}
.nd-app img{width:30px;height:30px;border-radius:7px}
.nd-home{position:absolute;inset:0;background:linear-gradient(160deg,#2b4a7a,#101a2c);display:grid;
  grid-template-columns:repeat(4,1fr);gap:12px;padding:18px 12px;align-content:start}
.nd-home i,.nd-home img{aspect-ratio:1;border-radius:9px;width:100%;background:rgba(255,255,255,.22)}
.nd-frame.on .nd-home img{animation:nd-pop .9s ease-out .3s both}
@keyframes nd-pop{0%{transform:scale(.4);opacity:0}60%{transform:scale(1.15);opacity:1}100%{transform:none}}
/* The finger: a ring that lands on the thing to tap in each frame. */
.nd-tap{position:absolute;width:30px;height:30px;border-radius:50%;border:3px solid rgba(255,59,48,.9);opacity:0}
.nd-frame.on .nd-tap{animation:nd-tap 1.6s ease-out .35s both}
.nd-tap.at-share{left:50%;margin-left:-15px;bottom:3px}
.nd-tap.at-more{right:4px;bottom:6px}
.nd-tap.at-menu-share{right:10px;bottom:136px}
.nd-tap.at-sheet{left:50%;margin-left:-15px;bottom:44px}
.nd-tap.at-add{right:4px;top:4px}
@keyframes nd-tap{0%{transform:scale(1.7);opacity:0}25%{transform:scale(1);opacity:1}40%{transform:scale(.85)}55%{transform:scale(1)}80%{opacity:1}100%{opacity:0}}
.nd-cap{margin:10px 0 0;font-size:14px;font-weight:700;color:var(--app-accent);min-height:18px}
@media (prefers-reduced-motion:reduce){
  .nag-arrow,.nd-frame.on .nd-sheet,.nd-frame.on .nd-menu,.nd-frame.on .nd-home img{animation:none}
  .nd-frame.on .nd-tap{animation:none;opacity:1}
}

