/* ===========================================================================
   Visit bar — shared component
   ---------------------------------------------------------------------------
   The "Time to climb?" strip and its expanding "plan a visit while it's quiet"
   panel. Extracted from style.css so it can be dropped onto BOTH the public
   site (style.css) and the console.css shells (the member portal, and the
   staff console since), which use different global variable names. Loaded
   after the surface's own stylesheet on each.

   Self-contained on purpose: the palette below is scoped to .visitbar (and so
   cascades to .busy/.visit/.typical inside it) rather than reaching for the
   host page's :root — that's what lets the identical markup render the same on
   a page styled by style.css and one styled by console.css.
   =========================================================================== */

.visitbar {
  /* Component palette — mirrors style.css :root so the bar looks the same
     wherever it's mounted. Scoped, so it doesn't leak into the host page. */
  --clr-bg:       #111827;
  --clr-bg-alt:   #1f2937;
  --clr-bg-dark:  #030712;
  --clr-border:   #374151;
  --clr-orange:   #f97316;
  --clr-orange-4: #fb923c;
  --clr-teal:     #14b8a6;
  --clr-teal-4:   #2dd4bf;
  --clr-text:     #f3f4f6;
  --clr-muted:    #9ca3af;
  --clr-muted-3:  #d1d5db;
  /* Typical-times heat ramp, cool (quiet) -> hot (packed). Validated as an
     ordinal ramp against this card's #111827 surface: lightness rises
     monotonically across all four steps (0.44 / 0.60 / 0.71 / 0.83), every
     adjacent gap clears 0.06, and the dim end still holds 2.34:1 — so the
     order is legible as brightness alone, before hue is read at all. It spans
     two hues deliberately (a "semantic heat" ramp), which is why the chart
     ships a scale legend: a multi-hue ramp is only readable as an order if the
     order is stated. Steps 3 and 4 are the live gauge's own busy orange and a
     brighter tint of it, so the two never contradict each other. */
  --heat-1: #115e59;
  --heat-2: #0d9488;
  --heat-3: #f97316;
  --heat-4: #fdba74;
  --font: 'Inter', system-ui, sans-serif;
  --radius: 8px;
  --gutter: clamp(2rem, 8vw, 10rem);

  background: var(--clr-bg-dark);
  border-bottom: 1px solid var(--clr-border);
}

/* ---- Sticky header stack ---------------------------------------------------
   The visit bar and the site nav ride together at the top of every scroll, so
   the "Time to climb?" answer stays on screen the whole way down the page — not
   just at the top, and on the portal too. Wrapping both in ONE sticky box (in
   base.html / base_portal.html) rather than pinning each separately keeps the
   nav directly under the strip even when the panel is open, instead of the two
   fighting over top:0 and overlapping. */
/* Published so style.css can size the mobile drawer against the WHOLE
   pinned stack (strip + nav), not just the nav. */
:root { --visitbar-h: 46px; }

.site-chrome {
  position: sticky;
  top: 0;
  z-index: 50;
}

/* The nav / appbar are pinned by .site-chrome now, so neutralise their own
   sticky — two nested sticky contexts at top:0 fight each other. Scoped to
   inside the chrome so the staff console's standalone appbar (which has no
   chrome wrapper) keeps its own sticky untouched. */
.site-chrome .nav,
.site-chrome .appbar {
  position: static;
  top: auto;
}

/* ---- Strip ---------------------------------------------------------------- */
.visitbar__strip {
  display: flex;
  justify-content: center;
  /* Anchor + clip the drifting sheen below. */
  position: relative;
  overflow: hidden;
}

/* Orange↔teal sheen drifting across the strip so the "Time to climb?" invite
   feels alive without pulling focus off the answer. Pure decoration: a
   pseudo-element (kept out of the a11y tree) and disabled under
   prefers-reduced-motion. Sits behind the toggle via z-index. */
.visitbar__strip::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 0;
  background: linear-gradient(
    100deg,
    rgba(249, 115, 22, 0) 32%,
    rgba(249, 115, 22, 0.35) 44%,   /* orange crest */
    rgba(20, 184, 166, 0.35) 56%,   /* teal crest */
    rgba(20, 184, 166, 0) 68%
  );
  background-size: 220% 100%;
  background-repeat: no-repeat;
  animation: visitbar-shimmer 6s linear infinite;
  pointer-events: none;
}

@keyframes visitbar-shimmer {
  from { background-position: 150% 0; }
  to   { background-position: -50% 0; }
}

.visitbar__toggle {
  display: flex;
  align-items: center;
  /* The BAR is the button, not the words in the middle of it. It used to be an
     inline-flex sized to its own content — a dot, three words and a chevron,
     about 250px of a 1920px strip — so the other 1,670px looked exactly like the
     control and did nothing when clicked. Nothing about the strip suggests where
     that edge falls, which makes a miss read as the page ignoring you rather
     than as aiming badly.

     Full width rather than a JS handler on the strip: this is already a
     <button>, so widening it keeps one control with one set of semantics, and
     the pointer cursor, the hover colour and the focus ring all grow with the
     hit area instead of pointing at a target that has moved. */
  width: 100%;
  justify-content: center;
  gap: .55rem;
  padding: .4rem 1rem;
  background: none;
  border: none;
  cursor: pointer;
  font-family: var(--font);
  font-size: .78rem;
  color: var(--clr-muted-3);
  transition: color .15s;
  /* Above the shimmer, and a finger-sized hit area. */
  position: relative;
  z-index: 1;
  min-height: 44px;
}
.visitbar__toggle:hover { color: var(--clr-text); }
/* Inset, because .visitbar__strip clips (overflow: hidden, for the sheen) and a
   full-bleed button's default outline would be shaved off top and bottom.
   Teal at -2px is the offset style style.css already uses for its own
   full-width focus targets. */
.visitbar__toggle:focus-visible {
  outline: 2px solid var(--clr-teal-4);
  outline-offset: -2px;
}

.visitbar__label {
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: .1em;
}

/* The live level rides in the strip itself — the label asks "Time to climb?"
   and this answers it, so a shut strip is still useful. */
.visitbar__now {
  font-weight: 800;
  text-transform: uppercase;
  letter-spacing: .08em;
  color: var(--clr-text);
}

.visitbar__dot {
  width: 8px;
  height: 8px;
  border-radius: 50%;
  flex-shrink: 0;
  background: var(--clr-teal);
  transition: background-color .3s ease;
}
.visitbar__dot--steady { background: #fbbf24; }
.visitbar__dot--busy   { background: var(--clr-orange); }
.visitbar__dot--closed { background: var(--clr-border); }

.visitbar__chevron { transition: transform .3s ease; }
.visitbar.is-open .visitbar__chevron { transform: rotate(180deg); }

/* The 0fr row is what holds a SHUT panel at zero height without `display: none`
   (which would drop it out of the a11y tree and out of the sheet's animation).

   The `0fr → 1fr` TRANSITION that used to sit here — the trick that animates to
   a panel's natural height without measuring it in JS — now lives on the console
   alone, at the bottom of this file, because the console's drawer is the only
   thing left that opens in normal flow. On every other surface it was actively
   harmful: the panel closes out of a `position: fixed` overlay, and reverting to
   a transitioned grid row made Chrome interpolate DOWN from the sheet's rendered
   height, so for 300ms a ~3,700px box appeared in flow at the very top of the
   document and then collapsed. A reader who had opened the panel from halfway
   down the page was scroll-anchored the whole way up as it shrank above them:
   closing at y=900 landed them at y=82, which reads as the page throwing them
   back to the top on its own. */
.visitbar__panel {
  display: grid;
  grid-template-rows: 0fr;
}
.visitbar.is-open .visitbar__panel { grid-template-rows: 1fr; }

/* visibility (not just clipping) keeps collapsed content out of the tab
   order and the accessibility tree; the delay lets it finish closing first. */
.visitbar__panel-inner {
  overflow: hidden;
  visibility: hidden;
  transition: visibility 0s linear .3s;
}
.visitbar.is-open .visitbar__panel-inner {
  visibility: visible;
  transition-delay: 0s;
}

/* The panel is a drawer in ONE case only — the staff console under 1024px (see
   the bottom of this file). Everywhere else it is a full-screen overlay, at
   every width.

   It used to be `max-height: min(76vh, 660px)` with its own scrollbar, which
   meant the one card people open this for — the week's busy chart — was read
   through a letterbox, with the opening hours and the reset schedule below the
   fold of a drawer that was itself inside the fold of the page. Two nested
   scrollbars for three cards.

   The first fix for that gave the component two shapes: a computer opened it to
   its natural height and grew the PAGE, a phone got a full-screen sheet. The
   desktop half then carried the cost the phone half had been written to avoid.
   Three cards stack into a column taller than any window, so the reader scrolled
   the whole site down to read the gauge, watched the strip they opened it from
   go off the top on the way, and scrolled all the way back up to get out of it.
   A browser window is not that much taller than a phone once this panel holds a
   week's chart, an opening-hours table and a reset schedule.

   So there is ONE shape now, phone to tablet to desktop: an overlay anchored
   under the strip, scrolled on its own, with the page behind it frozen — see
   the sheet block further down. This rule is what that overlay scrolls INSIDE,
   and `overscroll-behavior` is what keeps a flick or a wheel that runs off the
   end of it from scrolling the page underneath. */
.visitbar__scroll {
  overscroll-behavior: contain;
  border-top: 1px solid var(--clr-border);
}

/* Un-stick the whole header stack while the panel is open.

   The strip, the nav and the panel share one sticky box so the "Time to climb?"
   answer rides down the page (see .site-chrome above). That box is
   `position: sticky` with a `z-index`, which makes it a STACKING CONTEXT — and
   the overlay is a descendant of it. Every z-index inside the panel is then
   resolved against the chrome's 50 rather than against the page, so the portal's
   fixed bottom tab bar — z-index 60, but in the page's own stacking context —
   would paint straight over a sheet that asked for 70. Dropping the chrome to
   `position: static` discards the z-index with it: no stacking context, and the
   panel's 70 is finally measured against the page it has to cover.

   It costs nothing while the sheet is up. The page behind is frozen and hidden,
   so the nav sliding back to its flow position is invisible, and the strip is
   pinned to the top of the screen by its own rule in the sheet block below.
   Restored the instant it closes; the reader's scroll offset survives the round
   trip because visitbar.js re-asserts it once the classes are off.

   (This one line predates the overlay, for a reason that has since gone: the old
   desktop shape put the panel in normal flow, and a `position: sticky` box
   taller than the viewport pins itself to top:0 with no scroll that reveals its
   own bottom. Same rule, different load to bear.)

   .console-top is the console's copy of the same trap and needs the same line:
   `position: sticky; z-index: 20` in console.css, with the visit bar, the appbar
   and the check-in bar inside it. At an unescaped 20 the sheet would lose to the
   check-in bar it is supposed to be covering.

   Keyed off a class visitbar.js puts on <html>, not off `:has()`. Nothing about
   this needs the newer selector: the strip is a <button> and the panel cannot
   open at all without that script, so a state only the script can reach is free
   to be a state only the script sets. */
html.is-visitbar-open .site-chrome,
html.is-visitbar-open .console-top { position: static; }

/* The second way out — an up-arrow pinned at the bottom of the opened panel.

   Sticky at every width now, not just on a phone. The panel covers the screen on
   a computer too, which makes this the opened panel's main control rather than a
   footnote at the end of a drawer: the reader's pointer (or thumb) is wherever
   they stopped reading, and the strip that also closes it is a 46px bar at the
   very top of the screen. It was deliberately UNPINNED on a desktop before,
   because the old in-flow panel had no scrollbox of its own there and a sticky
   bar would have stuck to the bottom of the WINDOW and floated over the last
   card. The overlay gives it one, so it now sticks to the bottom of the PANEL.

   Scoped to the open state, which is how the staff console keeps the plain
   unpinned bar at the end of its drawer: visitbar.js never sets that class on
   a console under 1024px. */
.visitbar__foot { border-top: 1px solid var(--clr-border); }
html.is-visitbar-open .visitbar__foot {
  position: sticky;
  bottom: 0;
  /* Matches the sheet it is pinned to. Opaque is the load-bearing part, not the
     hue: this bar sits over the panel's own scrolling content, and a
     see-through one with the busy chart sliding under it reads as a rendering
     fault rather than as a control. The border-top above is what separates it. */
  background: var(--clr-bg-dark);
}

.visitbar__collapse {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: .5rem;
  width: 100%;
  min-height: 44px;
  padding: .5rem 1rem;
  background: none;
  border: none;
  cursor: pointer;
  font-family: var(--font);
  font-size: .72rem;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: .1em;
  color: var(--clr-muted-3);
  transition: color .15s, background-color .15s;
}
.visitbar__collapse:hover { color: var(--clr-text); background: rgba(255, 255, 255, .04); }

/* Reuse the section styles wholesale; only the chrome that made them
   standalone page bands needs neutralising inside the drawer. */
.visitbar .busy { background: transparent; border: none; padding-block: 1.5rem .25rem; }
.visitbar .visit { background: transparent; padding-block: 1.5rem 2rem; }
.visitbar .visit__intro { margin-bottom: 1.25rem; }
.visitbar .visit__intro h2 { font-size: 1.4rem; }

/* .container / .btn--teal-outline live in the public stylesheet but not the
   portal's (console.css), so the panel would lose its inset and its CTA outline
   on the member portal. Redefine both scoped to the component, at the public
   site's values, so the bar renders identically on both surfaces (and on the
   public site these simply shadow the globals with the same values). */
.visitbar .container {
  width: 100%;
  margin-inline: auto;
  padding-inline: var(--gutter);
}
.visitbar .btn--teal-outline {
  background: transparent;
  color: var(--clr-teal-4);
  border: 1px solid var(--clr-teal-4);
}
.visitbar .btn--teal-outline:hover {
  background: rgba(45, 212, 191, 0.1);
}

@media (prefers-reduced-motion: reduce) {
  .visitbar__panel,
  .visitbar__panel-inner,
  .visitbar__chevron { transition: none; }
  /* Remove the sheen outright rather than freezing it — a stopped gradient would
     leave a static orange/teal band across the strip, which isn't the intent. */
  .visitbar__strip::before { display: none; }
}

/* ===========================================================================
   Busy right now + plan your visit
   Level colours are semantic, not decorative: teal = come now,
   amber = normal, orange = it's packed. Kept to the brand palette so
   the band doesn't read as a third-party widget bolted onto the page.
   =========================================================================== */
.busy {
  background: var(--clr-bg-dark);
  border-block: 1px solid var(--clr-border);
  padding-block: 1.75rem;
}

.busy__inner {
  display: flex;
  flex-direction: column;
  gap: 1.5rem;
  align-items: flex-start;
}

@media (min-width: 800px) {
  .busy__inner { flex-direction: row; align-items: center; justify-content: space-between; gap: 3rem; }
  .busy__gauge { min-width: 22rem; }
}

.busy__eyebrow {
  display: block;
  font-size: .78rem;
  text-transform: uppercase;
  letter-spacing: .12em;
  color: var(--clr-teal-4);
  font-weight: 700;
}

.busy__headline {
  font-size: 1.5rem;
  font-weight: 800;
  line-height: 1.25;
  margin-top: .35rem;
  max-width: 26ch;
}

.busy__status { font-size: .875rem; color: var(--clr-muted); margin-top: .25rem; }

.busy__gauge { width: 100%; }

.busy__readout {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 1rem;
  margin-bottom: .5rem;
}

.busy__pct { font-size: 2.5rem; font-weight: 900; line-height: 1; }

.busy__level {
  font-size: .8125rem;
  font-weight: 800;
  text-transform: uppercase;
  letter-spacing: .1em;
  padding: .25rem .6rem;
  border-radius: 999px;
}

.busy__level--quiet  { background: rgba(20, 184, 166, .15); color: var(--clr-teal-4); }
.busy__level--steady { background: rgba(251, 191, 36, .15); color: #fbbf24; }
.busy__level--busy   { background: rgba(249, 115, 22, .15); color: var(--clr-orange-4); }

.busy__bar {
  height: 10px;
  border-radius: 999px;
  background: var(--clr-bg-alt);
  overflow: hidden;
}

.busy__fill {
  display: block;
  height: 100%;
  border-radius: 999px;
  /* Width changes on poll — animate so the bar slides rather than jumps. */
  transition: width .6s ease, background-color .3s ease;
  background: var(--clr-teal);
}
.busy__fill--steady { background: #fbbf24; }
.busy__fill--busy   { background: var(--clr-orange); }

.busy__caption { font-size: .8125rem; color: var(--clr-muted); margin-top: .5rem; }
.busy__gauge--closed .busy__pct { color: var(--clr-muted); }
/* The desk's notice is NOT styled here. It renders inside this panel as
   `.site-notice--inline` (static/css/trialbanner.css), which is the same
   component the bar above every shell's header uses — one announcement, one
   treatment, whichever surface a reader meets it on. `.busy__notice` used to
   live at this line, unused: the notice was being printed into
   `.busy__headline` instead, at display size, where it wrapped to four lines
   and pushed the gauge off the card. */

.visit { padding-block: 4.5rem; background: var(--clr-bg-alt); }

.visit__intro { max-width: 46rem; margin-bottom: 2.5rem; }
.visit__intro h2 { font-size: 2rem; font-weight: 900; }
.visit__intro p { color: var(--clr-muted-3); margin-top: .5rem; }

.visit__grid {
  display: grid;
  grid-template-columns: 1fr;
  gap: 1.5rem;
}

/* Hours + resets pair up first, then the chart takes its double-width column
   once there's room — going straight to 3-up at 900px squeezed the two list
   columns to ~200px and wrapped every opening-hours row mid-range (audit C37). */
@media (min-width: 640px) and (max-width: 1023.98px) {
  .visit__grid { grid-template-columns: 1fr 1fr; align-items: start; }
}
@media (min-width: 1024px) {
  .visit__grid { grid-template-columns: 2fr 1fr 1fr; align-items: start; }
}

.visit__card {
  background: var(--clr-bg);
  border: 1px solid var(--clr-border);
  border-radius: var(--radius);
  padding: 1.5rem;
}

.visit__card h3 { font-size: 1.05rem; font-weight: 800; margin-bottom: 1rem; }
.visit__note { font-size: .8125rem; color: var(--clr-muted); margin-top: .9rem; }

/* Week-at-a-glance chart: one row per day, one SLOT per hour of the shared
   axis. Every row has the same number of slots — including the hours that day
   is shut — so a bar's horizontal position means the same time on every row.
   Sizing the slots off bar count instead is what previously stretched a
   2pm–8pm weekday across the same width as a 9am–6pm Saturday. */
/* Positioning context for the hover tooltip, which is a single element moved
   around the chart rather than one per slot — 77 absolutely-positioned nodes
   to show at most one of them at a time. Keeping it inside .typical also keeps
   it inside .visitbar__scroll, which clips (it is an overflow-y:auto box). */
.typical { display: flex; flex-direction: column; gap: .3rem; position: relative; }

.typical__row { display: flex; align-items: flex-end; gap: .75rem; }

.typical__day {
  width: 2.4rem;
  flex-shrink: 0;
  font-size: .75rem;
  font-weight: 700;
  color: var(--clr-muted);
  text-transform: uppercase;
  letter-spacing: .06em;
}

.typical__row--today .typical__day { color: var(--clr-teal-4); }

/* No gap between slots: the 3px breathing room between bars is padding INSIDE
   each slot instead, so a run of closed hours paints one continuous shaded
   region. Gapped backgrounds split it into separate tall dark columns, which
   read as "very busy" — the opposite of what a closed hour means. */
.typical__slots {
  display: flex;
  align-items: flex-end;
  height: 34px;
  flex-grow: 1;
  min-width: 0;
}

/* Full-height so the whole column is a hover target and so closed hours read
   as a shaded region rather than a gap the eye skips over. */
.typical__slot {
  flex: 1 1 0;
  min-width: 0;
  height: 100%;
  padding: 0 1.5px;
  display: flex;
  align-items: flex-end;
}

.typical__slot--closed { background: var(--clr-bg-dark); opacity: .5; }

/* The hovered column lifts, so the pointer gets an answer from the chart even
   before the tooltip lands. */
.typical__slot:hover .typical__bar { filter: brightness(1.3); }
.typical__slot--closed:hover,
.typical__slot--unknown:hover { background: var(--clr-bg-alt); }

/* Open, but not enough history to draw — a baseline tick, never a bar. */
.typical__slot--unknown::after {
  content: "";
  width: 100%;
  height: 2px;
  border-radius: 2px;
  background: var(--clr-border);
}

.typical__bar {
  width: 100%;
  min-height: 2px;
  border-radius: 2px 2px 0 0;
  background: var(--clr-border);
}

/* The heat bands. Height and colour encode the same thing on purpose: the
   shape is readable across a room, and the exact percentage is a hover away.
   Standalone selectors (not .typical__bar--hN compounded onto .typical__bar)
   so the legend swatches can wear them without inheriting bar geometry. */
.typical__bar--h1 { background: var(--heat-1); }
.typical__bar--h2 { background: var(--heat-2); }
.typical__bar--h3 { background: var(--heat-3); }
.typical__bar--h4 { background: var(--heat-4); }

/* Today's row is marked on the ROW, not by repainting its bars: it used to be
   forced to flat teal, which is fine when every bar is the same grey and
   destroys the heat ramp now that they aren't. Today is the one day whose
   shape a climber most wants to read. */
.typical__row--today .typical__slots {
  background: rgba(45, 212, 191, .07);
  border-radius: 3px;
}

/* The current hour is the one bar people are actually looking for. A ring
   rather than a colour swap — orange is a heat step now, so recolouring this
   bar would make "right now" and "packed" the same signal.

   The ring hugs the bar with NO surface gap inside it. A gap is the right
   idiom for a marker sitting on top of other marks, and it was wrong here:
   nothing overlaps, and at a typical bar height of ~12px a 2px dark gap plus
   a 3.5px ring is 11px of chrome around 12px of data — the fill reads as an
   empty outline rather than as a highlighted bar.

   The column highlight is what actually carries the marker. A quiet hour is
   a two-pixel bar, and no ring drawn around two pixels says "you are here";
   marking the whole slot marks the TIME, which is the thing being pointed at. */
.typical__bar--now {
  box-shadow: 0 0 0 2px var(--clr-teal-4);
  border-radius: 2px;
}

.typical__slot--now {
  background: rgba(45, 212, 191, .14);
  box-shadow: inset 0 -2px 0 var(--clr-teal-4);
}

/* ---- Hover tooltip --------------------------------------------------------
   Value leads, label follows: the reader already knows which column they are
   pointing at and wants the number. pointer-events:none so the tooltip can
   never sit between the pointer and the slot that spawned it. */
.typical__tip {
  position: absolute;
  z-index: 5;
  pointer-events: none;
  transform: translate(-50%, -100%);
  background: var(--clr-bg-dark);
  border: 1px solid var(--clr-border);
  border-radius: 6px;
  padding: .3rem .55rem;
  box-shadow: 0 6px 18px rgba(0, 0, 0, .5);
  white-space: nowrap;
  text-align: center;
  line-height: 1.25;
}
.typical__tip[hidden] { display: none; }
/* Flipped under the row when there is no room above — the top row always. */
.typical__tip--below { transform: translate(-50%, 0); }
.typical__tip-value { display: block; font-size: .9375rem; font-weight: 800; color: var(--clr-text); }
.typical__tip-label { display: block; font-size: .6875rem; color: var(--clr-muted); }

/* ---- Scale legend ---------------------------------------------------------
   Required by the ramp, not decoration: colour is carrying magnitude across
   two hues, and the swatches are what say which end is which. It doubles as
   the phone story, where nothing hovers. */
.typical__key {
  display: flex;
  align-items: center;
  gap: .5rem;
  margin-top: .75rem;
  font-size: .6875rem;
  color: var(--clr-muted);
}
.typical__key-ramp { display: flex; gap: 2px; }
.typical__key-step { width: 18px; height: 8px; border-radius: 2px; }

/* Hour axis. Labels sit on every third slot and are allowed to overflow their
   own column so they stay legible in a narrow card. */
.typical__row--axis { margin-top: .15rem; }

.typical__row--axis .typical__slots { height: auto; align-items: flex-start; }

.typical__tick {
  flex: 1 1 0;
  min-width: 0;
  padding: 0 1.5px;
  font-size: .6875rem;
  color: var(--clr-muted);
  white-space: nowrap;
  overflow: visible;
}

.hours-list { list-style: none; display: flex; flex-direction: column; gap: .45rem; }

.hours-list li {
  display: flex;
  justify-content: space-between;
  gap: 1rem;
  font-size: .875rem;
  color: var(--clr-muted-3);
}

.hours-list__today { color: var(--clr-text); font-weight: 700; }

/* ---- Dated exceptions to the weekly hours --------------------------------
   Public holidays and one-off closures (_hours_exceptions.html). Lives in this
   sheet rather than style.css because it renders on two surfaces that load
   different stylesheets: the contact page (public, style.css) and the visit-bar
   panel, which the member portal shows off console.css. This sheet is the only
   one both of them load.

   The --clr-* tokens resolve in both places: inside the panel they come from
   the .visitbar block at the top of this file, and on the contact page from
   style.css's :root. It is never rendered anywhere that has neither.

   Orange, not teal: this is the line that contradicts the table above it, so it
   has to read as an exception rather than as more hours. */
.hours-exceptions {
  margin-top: .9rem;
  padding-top: .8rem;
  border-top: 1px solid var(--clr-border);
}

.hours-exceptions__title {
  font-size: .7rem;
  font-weight: 800;
  text-transform: uppercase;
  letter-spacing: .1em;
  color: var(--clr-orange-4);
  margin: 0 0 .5rem;
}

.hours-exceptions__list {
  list-style: none;
  display: flex;
  flex-direction: column;
  gap: .45rem;
  margin: 0;
  padding: 0;
}

.hours-exceptions__list li {
  display: flex;
  justify-content: space-between;
  gap: 1rem;
  font-size: .875rem;
  color: var(--clr-muted-3);
}

.hours-exceptions__today { color: var(--clr-text); font-weight: 700; }

.hours-exceptions__what { text-align: right; }

/* The reason ("Good Friday") under the hours it explains, so the two don't
   compete on one line when the note is long. */
.hours-exceptions__note {
  display: block;
  font-size: .78rem;
  color: var(--clr-muted);
  font-weight: 400;
}

.resets { list-style: none; display: flex; flex-direction: column; gap: .8rem; }

.resets li {
  display: grid;
  /* Two columns so the "Expected" tag sits BESIDE the date rather than on a
     third line of its own — the wall name and the note still span both. */
  grid-template-columns: auto 1fr;
  gap: .1rem .5rem;
  padding-bottom: .7rem;
  border-bottom: 1px solid var(--clr-border);
}
.resets li:last-child { border-bottom: none; }

.resets__sector { grid-column: 1 / -1; font-weight: 700; font-size: .9375rem; }
.resets__when { grid-column: 1; font-size: .8125rem; color: var(--clr-teal-4); font-weight: 600; }
.resets__note { grid-column: 1 / -1; font-size: .8125rem; color: var(--clr-muted); }

/* A projected date is dimmer than a booked one, and says so in words. The
   rotation is a good guess, not a promise — see gym/wall_rotation.py. */
.resets__row--expected .resets__when { color: var(--clr-muted-3); font-weight: 500; }
.resets__tag {
  grid-column: 2;
  justify-self: start;
  align-self: center;
  font-size: .625rem;
  font-weight: 800;
  text-transform: uppercase;
  letter-spacing: .07em;
  color: var(--clr-muted);
  border: 1px solid var(--clr-border);
  border-radius: 999px;
  padding: .05rem .4rem;
}

/* ---- The sheet: a full-screen overlay, at every width ----------------------
   One shape on a phone, a tablet and a computer alike. The panel takes the
   screen from directly under the strip to the bottom edge, is scrolled on its
   own, and is dismissed rather than scrolled past.

   Under 768px that was always the shape, for a reason that turned out to hold
   just as well above it: the three cards stack into one very long column, so
   "open to natural height" is a 2,000px page the reader has to scroll back up
   out of to reach anything else — and on the way down, the live gauge they
   opened it for goes off the top of the screen. A browser window is not that
   much taller than a phone once the panel carries a week's chart, an
   opening-hours table and a reset schedule.

   The strip itself stays put and stays tappable — pressing it again is one of
   the two ways out the owner asked for, and it is the one a hand finds without
   reading anything. The other is the up-arrow pinned at the bottom centre of
   the sheet (.visitbar__foot above, sticky), which is where the pointer or the
   thumb already is.

   Anchored top-and-bottom rather than given a `100vh` height: on iOS Safari
   `vh` is measured against the viewport with the URL bar HIDDEN, so a
   `100vh - 46px` sheet is about 60px taller than the screen actually is when
   the bar is showing — which puts the close arrow just off the bottom edge, on
   the one control that must never be unreachable. `top`/`bottom` track the live
   viewport and shrink with the chrome.

   The staff console under 1024px is the one case excluded, and keeps its capped
   drawer — see the bottom of this file. Every rule below is gated on
   `is-visitbar-open`, which visitbar.js never sets there. */

/* The grid-rows animation is a same-flow trick and cannot describe a fixed
   overlay; opacity + a short slide does the same job here.

   Gated on the OPEN state, and that gate is load-bearing rather than tidiness.
   `display: block` on a bare .visitbar__panel throws away the collapse itself:
   the 0fr grid row is the only thing holding a SHUT panel at zero height, so
   dropping to block left the whole panel in normal flow at its natural size,
   invisible (the inner box is visibility:hidden) but taking every pixel of it.
   A phone-width page therefore carried ~1,500px of dead space between the
   visit strip and the nav — the header, the hero and the first screen of every
   public and portal page sat below the fold with nothing to see above them —
   and the console, where the panel is capped to 400px and the class is never
   set at all, pinned 670px of sticky chrome to the top of an 844px screen and
   handed sticky_headers.js that figure as --appbar-h. */
html.is-visitbar-open .visitbar__panel {
  display: block;
  grid-template-rows: none;
  position: fixed;
  left: 0;
  /* `right: 0`, not `100vw`. Both spanned the window once the lock removed the
     scrollbar, but `100vw` was written for a gutter this rule no longer reserves
     (see the lock below) and would hang 15px past the initial containing block
     the moment anything reserved one again — painting over a strip the browser
     will not hit-test. Anchoring to the ICB cannot drift from what is clickable. */
  right: 0;
  top: var(--visitbar-h);
  bottom: 0;
  /* Above the portal's fixed bottom tab bar, which is z-index 60 in
     console.css and comes later in base_portal.html — so at an equal 60 the
     tabs won, and on a phone they sat on top of the sheet covering the pinned
     Close bar, the way out this panel is built around. A sheet that freezes
     the page behind it has to cover the page's own navigation too. Toasts
     (300) and the console's scanner overlay (200) still clear it.

     This number only counts because the un-stick rule further up drops
     .site-chrome out of `position: sticky` and takes its stacking context with
     it; inside that context a 70 here would be resolved against the chrome's
     own 50 and lose to the tab bar anyway. */
  z-index: 70;
  /* The same near-black the bar paints everywhere else, NOT --clr-bg-alt.
     Nothing sets a background on the console's drawer, so what the desk has
     always seen is `.visitbar`'s own --clr-bg-dark showing through a stack of
     transparent children. The sheet stated a lighter blue-grey instead, which
     was invisible while it was a phone-only shape and became the whole screen
     once it covered a desktop: two surfaces for one component, and a seam
     across the top where the pinned strip (also --clr-bg-dark) met it.

     It also puts the cards back the right way round. --clr-bg-alt is LIGHTER
     than the #111827 the cards are painted with, so the panel read as a pale
     field with three darker holes punched in it; against --clr-bg-dark the
     cards sit above their surround, which is how the console's drawer and the
     rest of the site already read. The heat ramp is unaffected either way — it
     was validated against the card's #111827, which does not move. */
  background: var(--clr-bg-dark);
  animation: visitbar-sheet-in .22s ease-out;
}

/* The strip rides to the top of the screen with the sheet.

   The sheet is anchored at `top: var(--visitbar-h)` — the strip's own height —
   which is where the strip sits only once the page has scrolled far enough for
   the chrome to be pinned. At the top of a page the trial banner is above it,
   so on a 390px screen the strip was ~180px below the sheet's top edge: the
   panel opened straight over it, 46px of unrelated banner showed through the
   gap above, and pressing the strip again — the way out a hand finds without
   reading anything — was underneath the panel. Pinning it costs nothing while
   the sheet is up: the page behind is frozen and covered anyway. Its own
   background, because .visitbar still paints where it sits in flow, behind the
   sheet. */
html.is-visitbar-open .visitbar__strip {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  /* The sheet starts at --visitbar-h, so the strip has to END there, and the
     two numbers cannot be left to agree by coincidence. They did not: the strip
     is its toggle's 44px min-height and the token says 46px, which opened a 2px
     seam across the top of the overlay with the trial banner showing through it
     — a hairline of the frozen page along the full width of a sheet that is
     supposed to have covered it. Stating the height here makes the seam
     impossible by construction, at every width and at the 38px the short-screen
     block further down drops the token to. */
  height: var(--visitbar-h);
  /* Over the sheet's 70 — the strip has to stay pressable. */
  z-index: 71;
  background: var(--clr-bg-dark);
}

html.is-visitbar-open .visitbar__panel-inner,
html.is-visitbar-open .visitbar__scroll {
  height: 100%;
  overflow: hidden;
}

html.is-visitbar-open .visitbar__scroll {
  overflow-y: auto;
  /* Momentum scrolling stays inside the sheet; the page behind is frozen by
     the lock below, so a flick that runs off the end of the panel does not
     quietly scroll the article underneath it. */
  -webkit-overflow-scrolling: touch;
  /* Room under the last card for the pinned close bar, plus the home-bar
     inset on a gesture-nav phone. Without it the final reset row sits behind
     the arrow that is supposed to be sitting on top of nothing. */
  padding-bottom: calc(env(safe-area-inset-bottom, 0px) + .25rem);
}

/* The close arrow is the opened panel's main control on every screen now, not
   a footnote at the end of a drawer, so it gets the taller target everywhere
   rather than only on a phone. The safe-area padding resolves to zero on a
   device that has no home bar, so it is free to state unconditionally. */
html.is-visitbar-open .visitbar__collapse {
  min-height: 52px;
  padding-bottom: calc(.5rem + env(safe-area-inset-bottom, 0px));
  font-size: .78rem;
}
html.is-visitbar-open .visitbar__collapse svg { width: 20px; height: 20px; }

/* Freeze the page behind the sheet. Applied to both elements because which
   one actually scrolls differs between engines, and `overflow: hidden` on the
   wrong one is a no-op that lets the article slide about under the panel.

   `scrollbar-gutter: stable` was tried here and REMOVED, which is worth stating
   so it does not get re-added. The problem it was aimed at is real: where a
   classic scrollbar is laid out inside the viewport (Windows, Linux outside
   GNOME), `overflow: hidden` takes it away, the content box grows ~15px, and the
   strip's centred label nudges sideways as the sheet appears.

   It does not fix it. Measured in a real Chrome window with a classic scrollbar:
   with `scrollbar-gutter: stable` on this rule, the page still went 1425 -> 1440
   on open — Chrome does not reserve the gutter for an `overflow: hidden` scroll
   container — AND `elementFromPoint` began returning null past x=1425 on a
   1440px window, so the right-hand 15px of the strip painted like the control
   and ignored a click. That is the bug the full-bleed toggle further up exists
   to remove, reintroduced at one edge, in exchange for nothing.

   So the nudge stays. It is about 7px on the label, under a sheet that is
   fading in over it, and it is the cheaper of the two by a distance: the page
   behind is covered while it is wrong and back to its normal width the instant
   the sheet goes, so nothing the reader is actually looking at moves. */
html.is-visitbar-open,
html.is-visitbar-open body {
  overflow: hidden;
}

/* A phone is the one width with no room for the page's wide gutter once the
   sheet covers the nav — the cards want its edge padding instead. The rest of
   the sheet is width-independent, which is why this is all that is left behind
   the media query. */
@media (max-width: 767.98px) {
  .visitbar .container { padding-inline: 1rem; }
  .visitbar .busy { padding-block: 1.25rem .25rem; }
  .visitbar .visit { padding-block: 1.25rem 1rem; }
}

@keyframes visitbar-sheet-in {
  from { opacity: 0; transform: translateY(-8px); }
  to   { opacity: 1; transform: none; }
}

@media (prefers-reduced-motion: reduce) {
  html.is-visitbar-open .visitbar__panel { animation: none; }
}

/* The drawer, now the console's SMALL-SCREEN shape rather than its only one.

   The desk used to sit the sheet out altogether. The argument was the counter:
   this panel shares the console's sticky stack with the appbar and the check-in
   bar, and reception opens it to read one number off the gauge while a climber
   waits, so a sheet over the scan box costs them the check-in they are halfway
   through. That argument is about the COUNTER, though, and not about the
   console — on a desktop monitor the same `min(48vh, 400px)` cap cut the busy
   chart off at two rows of seven, which is the letterbox the cap was introduced
   to fix. The owner could barely read the thing.

   So the split is by screen. From 1024px up the console gets the sheet like
   everything else; below it — the desk iPad in portrait, a phone — it keeps the
   drawer and the scan bar stays on screen, which is the one place the original
   argument still holds.

   This boundary is written once more in visitbar.js, which decides whether to
   set the class at all, and the two MUST agree. A console that gets the class
   while these rules still apply is a full-screen overlay with its content capped
   to 400px in the top of it. */
@media (max-width: 1023.98px) {
  .console-top .visitbar__scroll {
    max-height: min(48vh, 400px);
    overflow-y: auto;
  }
  /* ...and with it the `0fr → 1fr` transition, which is the drawer's own: it
     animates to the panel's natural height without measuring it in JS, and a
     drawer is the only thing left that opens in normal flow for it to describe.
     Scoped in here rather than onto .visitbar__panel because everywhere else it
     fires on the way OUT of a fixed overlay, where it briefly drops the whole
     sheet back into the document above the reader — see the note up there. */
  .console-top .visitbar__panel { transition: grid-template-rows .3s ease; }
}
/* Nothing else is needed to hold a small console out of the sheet, and that is
   deliberate rather than an omission. Every rule above is gated on the
   `is-visitbar-open` class on <html>, and under 1024px visitbar.js does not set
   that class on a console at all — so the desk keeps the drawer, keeps its own
   scrollbar, and keeps a page that scrolls behind it, with no override to write
   and no `:has()` to depend on.

   Written this way on purpose: the CSS-only version of that exclusion needed
   `:has()` to reach from <html> down to .console-top, and a browser without it
   would have applied the scroll LOCK to a console that has nothing covering the
   page — a desk that cannot scroll, which is the worst failure available here.
   A capability the feature does not need is not one to fail open on. */

/* Landscape / short viewport. The sheet is anchored top-and-bottom to the live
   viewport at every width, so a 400px-tall screen needs no cap of its own: it
   gets the same overlay, just shorter, with its own scroll and the close arrow
   pinned inside it rather than below the fold. A `min-width: 768px` cap used to
   live in this block precisely because a short landscape screen is usually WIDER
   than 768px and so missed the phone-only sheet entirely, opening to full
   natural height on a viewport with no height to give. There is no longer a
   phone-only sheet to miss.

   What is still true on a short screen is the strip. It is half of the pinned
   chrome a landscape phone can least afford (device audit H1), and the toggle
   runs the width of the bar, so trimming its height costs no reach. The sheet's
   own top offset is --visitbar-h, so the overlay follows it up. */
@media (max-height: 560px) {
  .visitbar__toggle { min-height: 36px; padding-block: .2rem; }
  :root { --visitbar-h: 38px; }
}
