/* Shared header identity — court-connectors.html, templates/auth_profile.html,
   templates/crystal_login.html all link to this file instead of keeping their
   own copy. Change a header font/color here, it applies everywhere.

   --skin-main falls back to --navy-light, not --navy: the "Default" theme's
   own main color IS --navy-light (#2A4D80, confirmed against theme_config.json)
   — using --navy here would actually change the out-of-the-box header color,
   not just centralize it. Pages that dynamically load theme_config.json
   (currently just court-connectors.html) get a theme-reactive header for free;
   pages that don't yet still match the real default look instead of drifting
   into their own hardcoded color, same failure mode this file exists to fix. */

header.topbar{
  background:var(--skin-main, var(--navy-light));
  color:var(--paper);
  padding:18px 28px;
  display:flex;
  align-items:center;
  justify-content:space-between;
  flex-wrap:nowrap;
  gap:12px;
  position:sticky;
  top:0;
  z-index:100;
  box-shadow:0 2px 8px rgba(0,0,0,0.08);
  overflow:visible;
}
@media (max-width:400px){
  header.topbar{padding:18px 12px;gap:6px;}
}

.brand-lockup{
  display:inline-flex;
  align-items:center;
  gap:6px;
  cursor:pointer;
  flex-shrink:0;
  overflow:visible;
  text-decoration:none;
}

.brand-name{
  font-family:'Caveat',cursive;
  font-weight:700;
  font-size:28px;
  color:var(--paper);
  line-height:1.3;
  white-space:nowrap;
  padding-right:6px;
}

/* Quicksand (rounder, closer in spirit to the cursive wordmark than
   Inter reads) — a status marker next to the logo, not part of the
   wordmark itself. */
.brand-beta-tag{
  font-family:'Quicksand',sans-serif;
  font-weight:600;
  font-size:12px;
  letter-spacing:0.04em;
  text-transform:uppercase;
  color:rgba(255,255,255,0.8);
  white-space:nowrap;
  margin-left:2px;
}


/* ============================================================
   0.9.5 APP CHROME — two-row header and bottom tab bar.
   Lives here, not in court-connectors.html, because /auth/profile wears
   the same header and the same tab bar. Everything below is scoped to a
   class (.topbar--two, .tabBar) that a page has to opt into, so
   crystal_login.html — which links this file and wants the plain one-row
   header — is unaffected.
   ============================================================ */
:root{
  /* The resting white for secondary text on navy chrome: the tab bar's
     inactive labels and the header's second row. Not a new colour — it is
     the rgba the tab bar already used, named so the two match by reference
     rather than by the same number being typed in both places. */
  --chrome-dim:rgba(255,255,255,0.62);
  /* Height of the tab bar. The body's bottom padding, the live-notification
     deck and the sheets that rise above it all derive from this, so the bar
     resizes in one place without any of them losing its top edge. */
  /* 64px, settled on a real 393pt phone with the proportions tuner rather
     than reasoned from Instagram's numbers. Theirs is ~43pt with ~24px icons
     and NO labels — its whole budget is one glyph. Ours carries a second
     line, so matching their height meant shrinking the icons until the bar
     felt cramped. 64px gives icons at Instagram's own size with a label
     under them: a 25 + 3 + 12 stack and 12px of air top and bottom. */
  --tabbar-h:64px;
  /* How far the bar's underside sits above the very bottom of the screen.
     Instagram's sits about 23pt up on a 15 Pro — INSIDE the 34pt safe-area
     inset, overlapping the indicator strip. That is deliberate and allowed:
     the indicator is translucent and the system handles contrast. Treating
     the inset as a floor you must clear instead put us at 10+34 = 44pt, a
     clear 20pt higher than every app it sits next to.
     max() keeps it sane where there is no inset at all: 12px on a desktop
     or an older phone, 22px on anything with a home indicator.
     Three things derive from this — the bar, the sheets above it and the
     notification deck — so it is a token, not a number typed three times. */
  --tabbar-bottom:max(12px, calc(env(safe-area-inset-bottom, 0px) - 12px));
}
/* ===== 0.9.5 TWO-ROW HEADER =====
   header.css still describes the ONE-row header, because the profile and
   Crystal login pages use it unchanged and should not inherit a layout
   built around a bottom nav they do not have. Everything here is scoped
   to .topbar--two, which only this page sets. */
/* header.topbar.topbar--two, NOT .topbar--two. header.css sets
   `header.topbar{display:flex}` — an element+class selector at (0,1,1).
   A lone class is (0,1,0) and loses, which left the header still flex:
   the two rows became side-by-side flex items, so the date sat out to the
   right of the wordmark and the absolutely-positioned edition label
   landed on top of it. Matching header.css's shape is what makes this
   win, and it is why every rule below is scoped the same way. */
/* The top inset matters now too: viewport-fit=cover lets the page run under
   the status bar, so without this the wordmark sits behind the clock. */
header.topbar.topbar--two{display:block;
  padding:calc(14px + env(safe-area-inset-top, 0px)) 16px 12px;}
/* A seasonal hat (0.9.12) stands above the ball; 14px of top padding would
   crop it at the top of the screen. Room per hat (0.9.12.1): the witch hat's
   tip stands about 27px above the ball and keeps 11px clear of the top edge,
   since on an iPhone the very top was hiding it; the Santa hat and bow are
   lower. */
header.topbar.topbar--two.has-hat{padding-top:calc(38px + env(safe-area-inset-top, 0px));}
header.topbar.topbar--two.has-hat.hat-christmas,
header.topbar.topbar--two.has-hat.hat-valentine{padding-top:calc(24px + env(safe-area-inset-top, 0px));}
.tb-row{display:flex;align-items:center;justify-content:space-between;gap:10px;}
.topbar--two .brand-name{font-size:26px;padding-right:4px;}

/* Row 1 holds the wordmark and nothing else, so centring is just
   justify-content. The edition label used to live here and had to be
   absolutely positioned to keep the lockup off-centre-free; moving it to
   row 2 removed that problem rather than working around it, and took the
   phone-width stacking with it. */
.topbar--two .tb-row1{justify-content:center;}

/* "Tennis . Bangkok Edition" — deliberately two spans, not one string.
   Pickleball and other cities are the next things to go here, and when
   one of them becomes switchable it is the sport and the city that have
   to change independently. Writing it as a single label now would mean
   unpicking it then. */
/* Who is signed in, opposite the date on row 2.
   "Tennis . Bangkok" used to sit here. It said the same thing on every
   screen forever, which is a poor use of the one spot in the header that is
   always visible; who is signed in changes, and is worth the space. The
   sport/city pair comes back when there is actually more than one of either
   to switch between.
   Matched to the date it shares the row with — same family, same size, same
   case — differing only in weight (600 against the date's 500) and which end
   of the row it sits at. */
/* 0.9.7: the name came back, so this is a capsule again — the same shape and
   tint as .tb-date opposite it, which makes the row read as a matched pair
   rather than a pill on one side and a loose mark on the other.

   The name is first and the mark second, so the picture keeps the outside
   edge of the row it has always had.

   max-width is what makes this safe on a small phone. The old avatar-only
   row was chosen because "a name would be the third thing competing for a
   row that also holds the date — on a 320px phone it is the difference
   between one line and two". The cap plus the ellipsis below is the answer
   to that: the name gives up its width first and the date is never pushed
   anywhere. flex-shrink:0 stays off the capsule deliberately — it MAY
   shrink; the date may not. */
.tb-account{display:inline-flex;align-items:center;gap:7px;min-width:0;
  max-width:min(58%, 215px);
  background:rgba(255,255,255,0.10);border-radius:999px;
  padding:3px 4px 3px 11px;
  text-decoration:none;color:var(--chrome-dim);
  transition:color .15s ease, background .15s ease;}
.tb-account:hover{color:#fff;background:rgba(255,255,255,0.16);}
.tb-account:active{background:rgba(255,255,255,0.20);}
/* Truncation rather than wrapping: a wrapped name is the two-line header the
   avatar-only decision was avoiding. min-width:0 is required for an ellipsis
   inside a flex item. */
/* No colour of its own: it inherits .tb-account's --chrome-dim and brightens
   to #fff with the capsule on hover, exactly as .tb-date does opposite it.
   Hard-coding #fff here made the right side read brighter than the left. */
.tb-account-name{font-size:11.5px;font-weight:600;
  white-space:nowrap;overflow:hidden;text-overflow:ellipsis;min-width:0;}
/* 22px inside the capsule, down from the 26px it was when it stood alone on
   this row — that size was argued from being the only tap target here, and
   the capsule is now the tap target. 22 + 3px padding matches the date
   capsule's height. */
.tb-account-avatar{width:22px;height:22px;border-radius:50%;flex-shrink:0;
  display:flex;align-items:center;justify-content:center;overflow:hidden;
  background:rgba(255,255,255,0.14);}
.tb-account-avatar svg{width:14px;height:14px;display:block;}
.tb-account-avatar img{width:100%;height:100%;object-fit:cover;display:block;}
.tb-account-avatar.has-photo{background:none;border:1.5px solid rgba(255,255,255,0.35);}
.tb-account-avatar.has-initials{font-size:10.5px;font-weight:600;letter-spacing:-.02em;color:#fff;}
/* .tb-account-cta held a "Pop In!" label beside the mark when signed out. It
   is gone: the row is the date and one avatar, in both states, and the words
   live on the sign-in page itself. The aria-label still says Pop In! so the
   link is not just "link" to a screen reader. */

/* Row 2 is the date, full width, no capsule and no ball. Because it is
   no longer squeezed into a pill beside two buttons, it can use the long
   form it never had room for — "Sunday, 5 October" rather than
   "Sun, 5 Oct". It still scrolls back to top on tap: that behaviour
   predates the pill and there is no reason to drop it with the styling. */
/* No rule between the rows. One was tried full-bleed and still read as a
   seam across a single block of navy — the spacing already separates the
   wordmark from the date, and the header's own box-shadow already marks
   where it ends. */
.tb-row2{margin-top:10px;align-items:center;gap:10px;}
/* Row 2 of 3 since 0.9.10.2: the slogan, in the loading screen's tagline
   font (Quicksand 600) and the header's own text colour, --chrome-dim, the
   same as the date and profile. The splash's warmer cream read as grey on
   the header's blue. A little more air above and below it than the
   two rows had between them — the header grows rather than crowding. */
.topbar--two .tb-slogan{text-align:center;font-family:'Quicksand',sans-serif;font-weight:600;
  font-size:13px;letter-spacing:.01em;color:var(--chrome-dim);margin-top:4px;white-space:nowrap;}
.topbar--two .tb-slogan + .tb-row2{margin-top:14px;}
/* The same calendar and map-pin glyphs the section nav uses for its Date
   and Venue stops, at the same 12px and the same stroke-width:2. Reused
   rather than redrawn: the header is now answering the same two questions
   that nav answers — when, and where — so it should ask them in the same
   visual language. */
.tb-ico{width:12px;height:12px;flex-shrink:0;}
/* Back in a capsule, and back to the short form. 0.9.5 took both away on the
   grounds that a full-width row could afford "Wednesday, 7 October" — true,
   but it left the date as the only bare text on a row that also holds a
   bordered avatar, so it read as a label rather than a control. The capsule
   makes it look like the thing it is: tappable, and the way back to the top. */
.tb-date{display:inline-flex;align-items:center;gap:6px;
  font-size:12.5px;font-weight:600;font-variant-numeric:tabular-nums;
  color:var(--chrome-dim);cursor:pointer;user-select:none;
  background:rgba(255,255,255,0.10);padding:5px 11px;border-radius:999px;
  transition:color .15s ease, background .15s ease;}
.tb-date:hover{color:#fff;background:rgba(255,255,255,0.16);}
.tb-date:active{background:rgba(255,255,255,0.20);}
/* ===== 0.9.5 TAB BAR =====
   Called the TAB BAR, never "the nav". This page already has a
   .section-nav, and the two are not the same kind of thing: the section
   nav is a position indicator that tracks where the page is scrolled to,
   while this switches between destinations. Same word for both would
   have made every future bug report ambiguous, so "nav" on its own now
   means the in-body one and this is the tab bar — which is also what
   iOS calls it, and Track 3's app-store build will have to speak that
   language anyway.

   The app's primary navigation moved to the bottom, which is the half of
   the screen a thumb reaches. The tennis ball is the Home tab rather than
   a generic house glyph: it is already the app's mark on the splash and
   in the header, and a logo that is also the way home is the pattern the
   user pointed at.

   Floating capsule, not a bar welded to the screen edge — the content
   scrolls visibly underneath it, which says "there is more below" in a
   way a flush bar does not. No backdrop blur: it was explicitly not
   wanted, and a solid accent surface needs no glass to separate itself
   from the paper behind it. */
/* FULL CAPSULE, after the iOS 26 floating tab bar. 999px rather than a
   measured radius so the ends stay true half-circles if --tabbar-h ever
   changes — a fixed 31px would quietly become a rounded rectangle again
   the moment the bar got taller.
   Note this is the SHAPE only, not the material: no backdrop blur, no
   translucency. Glass was explicitly not wanted, and a solid accent
   surface separates itself from the paper behind it without one.
   The side inset grows from 12px to 16px because a capsule reads as a
   floating object and needs visible air around it; at 12px the round
   ends nearly touch the screen edge and it looks wedged in rather than
   resting there. */
.tabBar{
  /* Placement measured against Instagram's: roughly 43pt tall, 23pt off the
     bottom, 25pt side insets. It can be that short because it carries no
     labels; ours keeps them, so 52px with the same generous margins.
     env() finally does something here: the viewport meta now carries
     viewport-fit=cover, and before that every safe-area inset in this file
     evaluated to 0, so the bar sat directly on the home indicator.
     --vv-bottom is the Android Chrome fix — see syncViewportBottom(). */
  /* 24px is main's own gutter, so the bar's edges land on the same axis as
     every card edge — the one repeated vertical line on the screen. It was
     22px, which is the worst of both: too close to read as a choice, too far
     to line up. Instagram's ~25pt aligns to nothing, because its feed is
     full-bleed; the number does not transfer, the reasoning does. If this
     ever feels too locked in, the move is clearly INSIDE the cards (~34px)
     so the bar plainly floats — not another 2px of drift. The sheets below
     take the same inset so they sit directly over the bar that opens them. */
  position:fixed;left:24px;right:24px;
  bottom:calc(var(--tabbar-bottom) + var(--vv-bottom, 0px));
  max-width:420px;margin:0 auto;height:var(--tabbar-h);border-radius:999px;
  display:flex;align-items:center;justify-content:space-around;
  /* 6px, matching the active pill's inset on every other side, and that is
     what makes the pill concentric with the bar: a pill of height barH-12
     has a stadium radius of (barH-12)/2, which IS barH/2 - 6, the concentric
     radius by definition. With 10px here the end pills sat 15px from the
     bar's edge but 6px from its top, and that asymmetry is what read as the
     arc being wrong. The radius was always right; the inset was not. */
  padding:0 6px;
  /* No position:relative here. It was added for the pill's containing block
     and silently overrode position:fixed above it — same rule, later
     declaration wins — so the bar stopped being fixed and dropped to the
     end of the document. A fixed element is already a containing block for
     its absolutely positioned children; it needed nothing. */
  /* Deep navy, not the header's --skin-main. A floating capsule has to read
     as an object sitting ON the page; at the header's lighter navy it read
     as a stray piece of the header that had come loose at the bottom.
     --skin-highlight rather than a flat var(--navy) so a Grand Slam theme
     still recolours it. It is the same token .secnav-pill uses for its dark
     pill, and it defaults to --navy, so out of the box this IS deep navy. */
  background:var(--skin-highlight, var(--navy));
  /* The shadow spreads UP and around, barely down (0.9.11.7). It used to be
     0 6px 20px, reaching ~26px under the bar; Safari on iOS 26 crops fixed
     elements a few points below the bar, just above its own toolbar, and
     the soft shadow ended in a hard line there. Now an even glow lifted
     slightly upward carries the "floating" look, and only a tight 4-5px
     contact shadow sits under the bar. */
  box-shadow:0 -4px 18px rgba(16,23,37,0.20), 0 2px 3px rgba(16,23,37,0.22);
  /* Clips the touch bloom to the capsule. Without it the glow spills past the
     bar onto the page, which reads as a rendering fault rather than light. */
  overflow:hidden;
  /* ===== Z-ORDER, and why these numbers =====
     court-connectors.html's own stack, for reference:
       60   main          65   ambient sync wave
       90   nav-stack     100  header.topbar
       900  Messenger FAB 1950 live-notification overlay
     The tab bar sits at 1960 — ABOVE the notification deck. It is the app's
     primary navigation and nothing transient should cover it. At its old
     120 the deck's own card drew straight over the bar whenever one was on
     screen. The sheets a tab opens go one higher again, so a sheet is never
     buried under the notification that happens to be showing. */
  z-index:1960;
}
/* ===== ACTIVE PILL =====
   One element, moved by JS under whichever tab is selected. Its left and
   width come from that tab's own offsetLeft/offsetWidth, so the slot maths
   lives in the layout instead of being duplicated as numbers here. */
.tabBarPill{
  position:absolute;top:6px;height:calc(var(--tabbar-h) - 12px);
  border-radius:999px;background:rgba(255,255,255,0.12);z-index:1;
  transition:left .32s cubic-bezier(.34,1.28,.64,1), width .32s cubic-bezier(.34,1.28,.64,1);
}
/* The bar bumps when the selection changes. Small and quick on purpose: the
   colour change is the event, this is only smoothing. */
.tabBar{transform-origin:50% 100%;transition:transform .22s cubic-bezier(.34,1.28,.64,1);}
.tabBar.lifting{transform:translateY(-4px) scale(1.02);}

.tabBarItem{
  flex:1;background:none;border:0;padding:0;cursor:pointer;height:100%;text-decoration:none;
  position:relative;
  display:flex;flex-direction:column;align-items:center;justify-content:center;gap:3px;
  position:relative;color:var(--chrome-dim);
  -webkit-tap-highlight-color:transparent;transition:color .15s ease;
}
/* No backdrop behind the active tab. Colour alone carries it, on the
   icon and the label together, which is quieter and stops the bar
   reading as a row of buttons with one pressed.
   The tennis ball needed its own inactive state for this to work: it is
   a full-colour logo, so unlike the three stroke icons it does not dim
   just because the text colour changed, and Home would have looked
   equally "on" from every other tab. Dimming it is what gives colour
   something to say here. */
/* ===== TOUCH BLOOM =====
   Light blooming from above whichever tab is being touched or hovered, fading
   down past the label. Deliberately NOT the iOS treatment: there is no rim on
   the pill and no specular edge, because copying that wholesale would make
   this look like someone else's app. The light does the work on its own.

   Two gradients rather than one. A single radial just gets paler as you scale
   it; a tight bright core over a wide soft wash keeps a readable centre while
   still lighting the bar around it, which is what gives it depth without an
   edge to define it.

   226% of a tab's width, so it spreads well past its own slot into the
   neighbours — that spill is what stops it reading as a lit rectangle in a
   cell. Sized in percent, not pixels, so it tracks the slot at any width. */
.tabBarItem::before{
  /* Tuned on a real phone with the Bloom Tuner, not guessed at from renders.
     Six attempts by eye got nowhere because none of us could see the same
     thing; eight sliders on the actual device settled it in one pass.

     What the numbers mean: top:10% puts the light high in the bar, 240% wide
     spills it into the neighbouring tabs, 230% tall keeps the vertical
     falloff gentle across the 56px on show, and a 30px blur is what turns it
     from a shape into light. The tint runs 60-80% toward --skin-main, so this
     is much more the chrome's own blue than white — white read as a highlight
     sitting on the bar rather than the bar being lit. */
  content:"";position:absolute;left:50%;top:10%;
  transform:translate(-50%,-50%);
  width:240%;height:230%;border-radius:50%;
  pointer-events:none;z-index:0;opacity:0;transition:opacity .22s ease;
  filter:blur(30px);
  background:radial-gradient(closest-side,
    color-mix(in srgb, var(--skin-main) 60%, rgba(255,255,255,0.35)),
    color-mix(in srgb, var(--skin-main) 80%, rgba(255,255,255,0.20)) 58%,
    transparent 82%);
}
/* ===== WHEN THE BLOOM IS LIT =====
   Press only. No hover rule at all — not even a gated one — because this is
   a touch-first interface and hover would give desktop a state phones never
   see. One behaviour everywhere is worth more than a flourish for the
   smaller audience.
   .is-touching is driven by touchstart/touchend/touchcancel rather than
   :active alone: iOS withholds :active while it decides whether a touch is a
   scroll, and skips it entirely on a fast tap. touchcancel is the important
   one — it fires when a touch becomes a scroll, and without it the glow stays
   lit while the page slides away under your finger. */
.tabBarItem:active::before,
.tabBarItem.is-touching::before{opacity:1;}
/* Someone who asked their device to stop animating still gets the light, it
   just appears rather than fading in. */
@media (prefers-reduced-motion:reduce){
  .tabBarItem::before{transition:none;}
}

/* Lime is reserved for a tab that actually goes somewhere. A not-yet tab
   brightens to plain white when selected — it is showing a sheet, not taking
   you to a section, and lending it the accent would promise more than it
   delivers. */
.tabBarItem.is-active{color:var(--skin-tab-active, var(--ball));}   /* follows the theme (0.9.17) */
/* .tabBarItem sets display:flex, which outranks the UA stylesheet's
   [hidden]{display:none} — without this a hidden tab renders anyway. Same
   trap as #view-signin's :not([hidden]). */
.tabBarItem[hidden]{display:none !important;}
.tabBarItem[data-soon].is-active{color:#fff;opacity:1;}

/* Outline and filled glyphs stack and crossfade. Neither is ever
   display:none, so the icon cannot blink out mid-change. */
.tabBarIcon{position:relative;}
.tabBarIcon .g{position:absolute;inset:0;transition:opacity .15s ease;}
.tabBarIcon .g svg{width:100%;height:100%;display:block;}
.tabBarIcon .g.fill{opacity:0;}
.tabBarItem.is-active .tabBarIcon .g.out{opacity:0;}
.tabBarItem.is-active .tabBarIcon .g.fill{opacity:1;}
.tabBarItem .tabBarBall{opacity:.45;filter:saturate(.35);transition:opacity .15s ease, filter .15s ease;}
.tabBarItem.is-active .tabBarBall{opacity:1;filter:none;}
/* 25px — Instagram's own icon size. Reaching it was the point of going to
   64px: at 56 the label forced them down to 22 and the bar read small. */
.tabBarIcon{width:25px;height:25px;display:block;position:relative;z-index:2;}
.tabBarIcon svg{width:100%;height:100%;display:block;}
/* The ball matches the icons exactly rather than running a pixel larger.
   It used to get +1 on the theory that a circle reads smaller than a square
   at the same box size; at 25px that correction is invisible and the
   mismatch is easier to notice than the illusion it was correcting. */
.tabBarBall{width:25px;height:25px;display:flex;align-items:center;justify-content:center;position:relative;z-index:2;}
/* The header ball carries a drop shadow that reads as depth against the
   navy header; at 23px on an already-shadowed bar it just looks smudged. */
.tabBarBall .ball-icon{filter:none;}
/* The date capsule's spin, brought back for the Home tab. It was removed in
   0.9.5 along with the capsule itself, but it is the right motion here: the
   ball ROTATES like a ball, rather than doing the wordmark's bounce-and-
   squash, which is a lockup animation and reads as the logo misbehaving when
   it happens to a 23px mark on its own. One clockwise turn, constant speed,
   the same 0.4s it always was. */
@keyframes ball-spin-cw{from{transform:rotate(0deg);}to{transform:rotate(360deg);}}
.tabBarBall .spin.spin-once{animation:ball-spin-cw .4s linear 1;}
.tabBarLabel{font-size:9.5px;font-weight:600;letter-spacing:.01em;position:relative;z-index:2;}
/* Not-yet tabs are dimmed rather than badged. A "Soon" pill on three of five
   tabs made the bar look like an advert for itself, and the badge competed
   with the label it sat on top of. Dimming says the same thing with no extra
   furniture, and it is the convention every OS already uses for unavailable.
   They are still TAPPABLE on purpose — the tap opens the sheet explaining
   what is coming. A dimmed control that does nothing at all reads as broken;
   one that answers the question reads as deliberate. */
/* Dim the CONTENT, not the item. opacity on .tabBarItem applies to its
   ::before as well, so the bloom came out half strength on exactly these
   three tabs — the ones most likely to be pressed out of curiosity. */
.tabBarItem[data-soon] .tabBarIcon,
.tabBarItem[data-soon] .tabBarLabel{opacity:.5;}
.tabBarItem[data-soon]:active{opacity:.75;}
/* Avatar slot inside the profile tab. Keeps #hamburger-btn's id so the
   existing sign-in nudge and the photo swap keep working untouched —
   only its position and size changed, not what drives it. */
.tabBar .hamburger-btn{
  width:25px;height:25px;background:none;color:inherit;border-radius:50%;
  display:flex;align-items:center;justify-content:center;position:relative;z-index:2;
}
.tabBar .hamburger-btn:hover{background:none;transform:none;}
.tabBar .hamburger-btn.has-avatar{width:24px;height:24px;}
.tabBar .hamburger-btn svg{width:21px;height:21px;}

/* Shown when a Soon tab is tapped. Same surface and motion as the
   settings sheet so the bottom of the app behaves consistently. */
.tabSheet{
  position:fixed;left:24px;right:24px;
  bottom:calc(var(--tabbar-bottom) + var(--tabbar-h) + 10px + var(--vv-bottom, 0px));
  max-width:420px;margin:0 auto;background:var(--white);border-radius:16px;padding:18px 18px 16px;
  box-shadow:0 10px 30px rgba(16,23,37,0.26);
  opacity:0;pointer-events:none;transform:translateY(10px);
  transition:opacity .2s ease, transform .2s cubic-bezier(.22,1,.36,1);
  /* One above the tab bar that opens them — see the z-order note there. */
  z-index:1961;
}
.tabSheet.open{opacity:1;pointer-events:auto;transform:translateY(0);}
.tabSheetTitle{font-size:15px;font-weight:700;color:var(--navy);margin-bottom:6px;}
.tabSheetBody{font-size:13px;line-height:1.55;color:var(--muted);}
.tabSheetTag{display:inline-block;font-size:10px;font-weight:700;letter-spacing:.05em;text-transform:uppercase;
  color:var(--skin-main, var(--navy-light));background:var(--paper);padding:3px 8px;border-radius:999px;margin-bottom:9px;}


/* ---- Brand lockup motion ----
   Shared so every page wearing this header reacts the same way when the
   wordmark is tapped. The lockup is NOT a link on any page: home is the
   tab bar's Home tab. Tapping the ball plays. */
/* header.topbar / .brand-lockup / .brand-name shared identity now lives in
   header.css — this just layers this page's own extras on top (bigger
   hover hit-area, ball/text-wind animation anchoring). */
.brand-lockup{padding:8px 46px 8px 10px;margin:-8px -10px;}
.brand-name{transform-origin:left center;overflow:visible;}
.brand-name.windy{animation:brand-text-wind 5s linear;}

.ball-icon{
  transform-box:fill-box;transform-origin:center;position:relative;z-index:2;overflow:visible;
  filter:drop-shadow(0 7px 3px rgba(0,0,0,0.22));
  /* filter is transitioned too: the hover state deepens the shadow as the
     ball lifts, and without it the shadow would jump while the scale eased. */
  transition:transform .25s cubic-bezier(.34,1.28,.64,1), filter .25s ease;
}
@media (hover: hover) and (pointer: fine) {
  .brand-lockup:hover .ball-icon{transform:scale(1.35) translateY(-2px);}
}
.squash{transform-box:fill-box;transform-origin:center;}
.spin{transform-box:fill-box;transform-origin:center;}

.ball-icon.bouncing{animation:brand-ball-translate 5s linear;}
.ball-icon.bouncing .spin{animation:brand-ball-spin 5s linear;}
.ball-icon.bouncing .squash{animation:brand-ball-squash 5s linear;}

/* Tennis-ball bounce: 4 diminishing parabolic arcs, a short flat
   roll-out, a long hold, then an elastic pull-back (with overshoot)
   to rest. Rotation is derived from actual distance traveled (pure
   rolling physics), not just elapsed time, and stays velocity-
   continuous across every phase transition. */
@keyframes brand-ball-translate{
  0%{transform:translate(0px, 0px);}
  2.67%{transform:translate(9.1px, -14.4px);}
  5.33%{transform:translate(18.1px, -23.1px);}
  8.0%{transform:translate(27.2px, -26.0px);}
  10.67%{transform:translate(36.2px, -23.1px);}
  13.33%{transform:translate(45.3px, -14.4px);}
  16.0%{transform:translate(54.4px, 0px);}
  18.0%{transform:translate(61.6px, -7.8px);}
  20.0%{transform:translate(68.8px, -12.4px);}
  22.0%{transform:translate(76.1px, -14.0px);}
  24.0%{transform:translate(83.3px, -12.4px);}
  26.0%{transform:translate(90.6px, -7.8px);}
  28.0%{transform:translate(97.8px, 0px);}
  29.12%{transform:translate(101.9px, -2.9px);}
  30.25%{transform:translate(105.8px, -5.0px);}
  31.38%{transform:translate(109.6px, -6.3px);}
  32.5%{transform:translate(113.3px, -6.9px);}
  33.63%{transform:translate(116.9px, -6.9px);}
  34.75%{transform:translate(120.4px, -6.3px);}
  35.88%{transform:translate(123.9px, -5.1px);}
  37.0%{transform:translate(127.2px, -3.4px);}
  38.12%{transform:translate(130.5px, -1.2px);}
  39.25%{transform:translate(133.6px, -1.0px);}
  40.38%{transform:translate(136.7px, -2.2px);}
  41.5%{transform:translate(139.7px, -2.9px);}
  42.62%{transform:translate(142.7px, -3.0px);}
  43.75%{transform:translate(145.5px, -2.5px);}
  44.88%{transform:translate(148.3px, -1.5px);}
  46.0%{transform:translate(151.0px, 0px);}
  47.44%{transform:translate(154.1px, 0px);}
  48.88%{transform:translate(156.5px, 0px);}
  50.32%{transform:translate(158.5px, 0px);}
  51.75%{transform:translate(160.1px, 0px);}
  53.19%{transform:translate(161.4px, 0px);}
  54.63%{transform:translate(162.5px, 0px);}
  56.07%{transform:translate(163.3px, 0px);}
  57.51%{transform:translate(164.0px, 0px);}
  85%{transform:translate(164.0px, 0px);}
  86%{transform:translate(152.1px, 0px);}
  88%{transform:translate(97.7px, 0px);}
  90%{transform:translate(32.6px, 0px);}
  92%{transform:translate(-16.3px, 0px);}
  94%{transform:translate(-8.7px, 0px);}
  96%{transform:translate(-3.3px, 0px);}
  98%{transform:translate(1.1px, 0px);}
  100%{transform:translate(0px, 0px);}
}
@keyframes brand-ball-spin{
  0%{transform:rotate(0deg);}
  2.67%{transform:rotate(57.6deg);}
  5.33%{transform:rotate(115.1deg);}
  8.0%{transform:rotate(172.7deg);}
  10.67%{transform:rotate(230.2deg);}
  13.33%{transform:rotate(287.8deg);}
  16.0%{transform:rotate(345.3deg);}
  18.0%{transform:rotate(391.3deg);}
  20.0%{transform:rotate(437.4deg);}
  22.0%{transform:rotate(483.4deg);}
  24.0%{transform:rotate(529.5deg);}
  26.0%{transform:rotate(575.5deg);}
  28.0%{transform:rotate(621.5deg);}
  29.12%{transform:rotate(647.1deg);}
  30.25%{transform:rotate(672.0deg);}
  31.38%{transform:rotate(696.2deg);}
  32.5%{transform:rotate(719.8deg);}
  33.63%{transform:rotate(742.8deg);}
  34.75%{transform:rotate(765.2deg);}
  35.88%{transform:rotate(787.0deg);}
  37.0%{transform:rotate(808.2deg);}
  38.12%{transform:rotate(828.9deg);}
  39.25%{transform:rotate(849.1deg);}
  40.38%{transform:rotate(868.7deg);}
  41.5%{transform:rotate(887.8deg);}
  42.62%{transform:rotate(906.4deg);}
  43.75%{transform:rotate(924.5deg);}
  44.88%{transform:rotate(942.1deg);}
  46.0%{transform:rotate(959.3deg);}
  47.44%{transform:rotate(978.8deg);}
  48.88%{transform:rotate(994.5deg);}
  50.32%{transform:rotate(1007.2deg);}
  51.75%{transform:rotate(1017.4deg);}
  53.19%{transform:rotate(1025.6deg);}
  54.63%{transform:rotate(1032.2deg);}
  56.07%{transform:rotate(1037.6deg);}
  57.51%{transform:rotate(1041.9deg);}
  85%{transform:rotate(1041.9deg);}
  86%{transform:rotate(966.3deg);}
  88%{transform:rotate(620.7deg);}
  90%{transform:rotate(207.1deg);}
  92%{transform:rotate(-103.6deg);}
  94%{transform:rotate(-55.3deg);}
  96%{transform:rotate(-21.0deg);}
  98%{transform:rotate(7.0deg);}
  100%{transform:rotate(0deg);}
}
@keyframes brand-ball-squash{
  0%{transform:scale(1.075,0.925);}
  2.67%{transform:scale(0.992,1.022);}
  5.33%{transform:scale(0.942,1.081);}
  8.0%{transform:scale(0.925,1.1);}
  10.67%{transform:scale(0.942,1.081);}
  13.33%{transform:scale(0.992,1.022);}
  16.0%{transform:scale(1.075,0.925);}
  18.0%{transform:scale(0.992,1.022);}
  20.0%{transform:scale(0.942,1.081);}
  22.0%{transform:scale(0.925,1.1);}
  24.0%{transform:scale(0.942,1.081);}
  26.0%{transform:scale(0.992,1.022);}
  28.0%{transform:scale(1.075,0.925);}
  29.12%{transform:scale(1.055,0.948);}
  30.25%{transform:scale(1.024,0.985);}
  31.38%{transform:scale(0.995,1.018);}
  32.5%{transform:scale(0.976,1.04);}
  33.63%{transform:scale(0.976,1.04);}
  34.75%{transform:scale(0.995,1.018);}
  35.88%{transform:scale(1.025,0.983);}
  37.0%{transform:scale(1.058,0.944);}
  38.12%{transform:scale(1.043,0.962);}
  39.25%{transform:scale(1.048,0.957);}
  40.38%{transform:scale(0.982,1.034);}
  41.5%{transform:scale(0.951,1.07);}
  42.62%{transform:scale(0.951,1.07);}
  43.75%{transform:scale(0.982,1.034);}
  44.88%{transform:scale(1.045,0.96);}
  46.0%{transform:scale(1.075,0.925);}
  50%{transform:scale(1,1);}
  85%{transform:scale(1,1);}
  86%{transform:scale(1.038,0.963);}
  88%{transform:scale(1.075,0.925);}
  90%{transform:scale(1.03,0.97);}
  92%{transform:scale(0.933,1.068);}
  94%{transform:scale(0.97,1.03);}
  96%{transform:scale(0.985,1.015);}
  98%{transform:scale(0.995,1.005);}
  100%{transform:scale(1,1);}
}
/* "CourtPop" reacts as the ball rushes back past it during the
   pull-back (squeezes/skews as if buffeted, then relaxes). */
@keyframes brand-text-wind{
  0%{transform:skewX(0deg) scaleX(1);}
  85%{transform:skewX(0deg) scaleX(1);}
  86.5%{transform:skewX(-3deg) scaleX(0.98);}
  88%{transform:skewX(-8deg) scaleX(0.92);}
  89.5%{transform:skewX(-4deg) scaleX(0.98);}
  91%{transform:skewX(0deg) scaleX(1);}
  100%{transform:skewX(0deg) scaleX(1);}
}
