/* ═══════════════════════════════════════════════════════════════════
   THE SHELL
   ═══════════════════════════════════════════════════════════════════
   2026-09-07. The pass that fixes roughly a third of the screenshots
   without touching a single page.

   Three defects, one cause. From the iPhone capture session:

     P1.1  "Scroll content renders over the sticky header and the iOS status
            bar... it makes the product read as broken to anyone we hand a
            phone to."
     P1.2  The gold help button sits on top of text and buttons.
     P1.3  Content runs under the bottom chrome.

   All three are the same omission: the app was built as a page that
   scrolls, on a device that has a notch and a home indicator, and never
   reserved the space either one occupies. index.html used
   env(safe-area-inset-*) exactly ONCE. output/prototypes has it five
   times, which is why the prototype feels like an app and the real thing
   does not.

   ── WHY THIS AND NOT THE PROTOTYPE'S GRID ────────────────────────────

   The prototype uses  display:grid; grid-template-rows:auto 1fr auto  with
   the middle row as the only scroller, so content physically cannot pass
   over the header. That is the better architecture and it is where this
   should end up.

   It is not what this file does, because that change re-parents every one
   of 53 routes at once and any screen that scrolls its own container would
   need finding and fixing individually. This does the same job with the
   tools already in the page - a sticky header with a real background, an
   explicit stacking order, and reserved space top and bottom - so the
   defect is fixed today and the grid migration stays available as a later,
   deliberate move rather than an emergency one.

   ── ORDER ────────────────────────────────────────────────────────────
   Loaded last so it wins on specificity ties without reaching for
   !important, which task #21 is trying to remove rather than add to.
   ═════════════════════════════════════════════════════════════════════ */

:root {
  /* One place for the insets, so nothing has to remember the fallback.
     iOS reports 0 when there is no notch, which is what we want. */
  --twf-safe-top: env(safe-area-inset-top, 0px);
  --twf-safe-bottom: env(safe-area-inset-bottom, 0px);
  --twf-safe-left: env(safe-area-inset-left, 0px);
  --twf-safe-right: env(safe-area-inset-right, 0px);

  /* One stacking order, written down. Before this the numbers were
     scattered across six files and the header lost to page content by
     accident rather than by decision. */
  --twf-z-content: 1;
  --twf-z-fab: 40;
  --twf-z-header: 60;
  /* 2026-09-09: was 70, and it applied to nothing.
     The rule below named .twf-mobile-tray, .twf-thumb-bar and
     [data-twf-bottom-nav]. The element the app actually renders is
     .twf-bottom-nav (twf-app-core.js, nav.className), which takes z-index
     1000 from twf-styles-cockpit.css. So this file declared "one stacking
     order, written down", loaded last to win ties, and then missed the one
     element it most needed to place.
     1000 keeps the render identical and makes the token true. Anything that
     must sit ABOVE the nav now says so against this number instead of
     guessing a bigger one. */
  --twf-z-bottomnav: 1000;
  --twf-z-sheet: 1200;

  /* How much room the bottom chrome occupies on a phone, safe area excluded.
     One number: the toast, the build banner and the content padding all
     measured this independently and two of them were wrong. */
  --twf-chrome-bottom: 76px;
}

/* ── 0. Let the header stick to the VIEWPORT, not to a phantom scroller ──

   This is the actual bug, and it took a render to find. The first version of
   this file made .topbar sticky and it changed nothing, because:

     main.main   computes overflow-y: auto  -> it is a scroll container
     main.main   is 11658px tall, the full height of its content
     html        is what actually scrolls

   position:sticky pins to the nearest SCROLLING ANCESTOR. So the header was
   pinning itself to the top of `main` - a box that never scrolls, and whose
   top edge had already travelled 891px off screen. The header was working
   exactly as told, against the wrong reference. From the outside it looked
   like content rendering over a header; there was no header on screen at all.

   `main` gains nothing from being a scroll container - it is sized to its own
   content, so it can never overflow and never scrolls. Releasing it hands the
   sticky back to the viewport, where it belongs.

   The prototype avoids this whole class of problem with
   grid-template-rows: auto 1fr auto and a single, explicit scroller. That is
   still the destination. This makes the current structure behave correctly in
   the meantime, without re-parenting 53 routes in one commit.

   body carries the same defect as main: overflow-y:auto at full content
   height, so it too is a scroll container that never scrolls. Releasing
   main alone was not enough - the sticky simply pinned to body instead,
   with the same off-screen top edge. html remains the one real scroller,
   which is what it was doing all along. */

html body.ia-v2,
main.main,
#app.app {
  overflow: visible;
}

/* ── 1. The header keeps its own space and stays on top ──────────────── */

/* `html body .topbar` rather than `.topbar`: something in the cascade was
   winning on padding-top and the computed value stayed at 6px. Three type
   selectors of specificity, still no !important - task #21 is trying to
   remove those, not add more. */
html body .topbar {
  position: sticky;
  top: 0;
  z-index: var(--twf-z-header);

  /* The status bar's height, above the header's own padding. Without this
     the first line of the header sits under the clock and the battery. */
  padding-top: calc(var(--twf-safe-top) + 10px);
  padding-left: max(var(--twf-safe-left), 0px);
  padding-right: max(var(--twf-safe-right), 0px);

  /* A sticky element with a transparent background does not hide what
     passes beneath it - it just travels with the scroll while the content
     shows through. This is the actual "renders over the header" bug: the
     header was there the whole time, you could see straight through it.
     Blur rather than a flat fill so it reads as iOS chrome, with an opaque
     fallback for anything that cannot blur. */
  background: var(--pt-canvas, #F5F6F8);
  background: color-mix(in srgb, var(--pt-canvas, #F5F6F8) 94%, transparent);
  backdrop-filter: saturate(180%) blur(20px);
  -webkit-backdrop-filter: saturate(180%) blur(20px);
}

@supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
  .topbar { background: var(--pt-canvas, #F5F6F8); }
}

/* The in-app route header sits directly under the topbar and has the same
   job on the screens that use it. */
.twf-mobile-route-header {
  position: sticky;
  top: 0;
  z-index: calc(var(--twf-z-header) - 1);
  background: var(--pt-canvas, #F5F6F8);
  background: color-mix(in srgb, var(--pt-canvas, #F5F6F8) 94%, transparent);
  backdrop-filter: saturate(180%) blur(20px);
  -webkit-backdrop-filter: saturate(180%) blur(20px);
}

/* Signed out there is no chrome to clear, and the login card centres on the
   viewport - so the inset would only push it off-centre. */
body.unauthenticated .topbar { padding-top: 0; }

/* ── 2. The help button is a header control now, not a FAB ───────────
   It was fixed-position, gold-filled, bottom-left, on every screen. See the
   note in twf-spotlight-hints.js: it covered content on all 53 routes and
   had something to say on only 4.

   In the header it is a peer of the bell, so it takes the bell's geometry -
   44px tap target, no fill, muted stroke - and stops competing for
   attention with the primary actions on the page. position:static is set
   explicitly because the old rule made it fixed and this file loads after
   whatever else may still say so. */

#twfHintHelpBtn,
.twf-hint-help-btn {
  position: static;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 44px;
  height: 44px;
  min-width: 44px;
  min-height: 44px;
  padding: 0;
  border: none;
  border-radius: 6px;
  background: none;
  color: rgba(12, 18, 32, .7);
  cursor: pointer;
  transition: background .15s, color .15s;
  /* Undo the FAB's shadow/fill if an older rule still carries them. */
  box-shadow: none;
  font-size: 0;               /* kills the legacy "?" text node if present */
}

#twfHintHelpBtn svg { font-size: initial; }

#twfHintHelpBtn:hover {
  color: var(--pt-gold-strong, #A8852A);
  background: rgba(201, 168, 76, .08);
}

#twfHintHelpBtn:focus-visible {
  outline: 2px solid var(--pt-gold, #C9A84C);
  outline-offset: 2px;
}

/* ── 2b. The inline hint marker ──────────────────────────────────────
   Sits on the element it explains, not in a corner. Quiet enough to live
   there permanently: 14px, no fill, muted until touched.

   It does NOT get a 44px box. That guidance is for primary controls; a
   44px target beside every explainable element would push the layout
   around and defeat the point of it being unobtrusive. The tap area is
   widened with padding instead, and the thing it explains is always
   reachable by other means - this is additive, never the only route to
   anything. */

.twf-hint-dot {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  vertical-align: middle;
  margin-left: 6px;
  padding: 4px;                 /* ~22px effective target, no layout shift */
  border: 0;
  background: none;
  color: rgba(12, 18, 32, .3);
  cursor: pointer;
  line-height: 0;
  border-radius: 999px;
  transition: color .15s, background .15s;
}

.twf-hint-dot:hover,
.twf-hint-dot:focus-visible {
  color: var(--pt-gold-strong, #A8852A);
  background: rgba(201, 168, 76, .1);
}

.twf-hint-dot:focus-visible {
  outline: 2px solid var(--pt-gold, #C9A84C);
  outline-offset: 1px;
}

/* Print and screenshots should not carry a control that cannot be used. */
@media print { .twf-hint-dot, #twfHintHelpBtn { display: none; } }

/* ── 2c. One way back, not three ─────────────────────────────────────
   Two pieces of code answer the same question on every drill-down route:

     renderChrome()  -> #twfNavChrome.twf-nav-chrome.visible
                        "‹ Back" on phones, "Parent › Current" on desktop
     navigate()      -> .back-nav-bar  "← Back to Settings"

   Both consult getBackNavTarget(route), so they agree on the destination
   and disagree only on how many times to say it. The profile opened with
   the date header, then "‹ Back | My Profile", then "← Back to Settings" -
   three ways back before any content.

   The nav chrome is the iOS-shaped one and it is already conditional on
   the route having a parent, so the bar is redundant wherever the chrome
   is visible. Keyed on the chrome actually being present rather than on
   viewport width, so a route the chrome skips keeps its bar. (The first
   version of this rule keyed on .twf-mobile-route-header, a class that
   exists only in the print stylesheet. A render caught it.) */

body:has(#twfNavChrome.visible) .back-nav-bar { display: none; }

/* The full-screen flows - the event page, shift setup, the wrap, a red
   event, My Shifts - draw their own top bar (.twf-sp-top / .twf-su-top)
   with a Back and a title, by design: they are the pushed-screen pattern.
   The nav chrome's "‹ Back" above that was the second Back. When the page
   has its own, the chrome and the legacy bar both yield. */
body:has(#pagesContainer .twf-sp-top) #twfNavChrome,
body:has(#pagesContainer .twf-su-top) #twfNavChrome,
body:has(#pagesContainer .twf-mobile-routebar) #twfNavChrome,
body:has(#pagesContainer .twf-sp-top) .back-nav-bar,
body:has(#pagesContainer .twf-su-top) .back-nav-bar,
body:has(#pagesContainer .twf-mobile-routebar) .back-nav-bar { display: none; }

/* ── 2c2. The Back strip, on a phone ─────────────────────────────────
   renderChrome() draws "‹ Back | Title" as a white bar with a hairline -
   the one piece of chrome that was still a box after the shell pass. On the
   canvas now, no rule, the Back control a quiet 44px target, the title at
   17px semibold the way an iOS nav bar titles a pushed screen. The cockpit
   CSS sets this bar at 13px/36px; same selector depth, later file, wins. */

@media (max-width: 768px) {
  html body .twf-nav-chrome.visible {
    background: none;
    border-bottom: 0;
    padding: 4px 16px 0;
    gap: 8px;
  }

  html body .twf-nav-chrome .twf-bc-back {
    min-height: 44px;
    padding: 0 10px 0 4px;
    background: none;
    font-size: 15px;
    font-weight: 600;
    color: var(--pt-gold-strong, #A8852A);
  }

  html body .twf-nav-chrome .twf-bc-current {
    margin-left: 4px;
    font-size: 17px;
    font-weight: 700;
    color: var(--pt-navy, #0C1220);
    letter-spacing: -0.01em;
  }
}

/* Once the Back strip names the screen, the page's own display-serif
   title says it again 60px lower ("Swaps" then "Shift Swap Approvals").
   The one-line subtitle stays; it is the only copy that says what to do. */
@media (max-width: 768px) {
  body:has(#twfNavChrome.visible) .twf-cockpit-header .twf-section-header__title,
  body:has(#twfNavChrome.visible) .elm-header-title,
  body:has(#twfNavChrome.visible) #pagesContainer > h1:first-child,
  body:has(#twfNavChrome.visible) #pagesContainer > h2:first-child,
  body:has(#twfNavChrome.visible) #pagesContainer > div:first-child > h1:first-child,
  body:has(#twfNavChrome.visible) #pagesContainer > div:first-child > h2:first-child,
  body:has(#twfNavChrome.visible) #pagesContainer > div:first-child > div:first-child > h1:first-child,
  body:has(#twfNavChrome.visible) #pagesContainer > div:first-child > div:first-child > h2:first-child { display: none; }
  body:has(#twfNavChrome.visible) .twf-cockpit-header { margin-top: 0; padding-top: 0; }
  body:has(#twfNavChrome.visible) .elm-header { margin-bottom: 6px; }
  /* and the data-driven case: renderChrome() marks the first heading whose
     text repeats the strip's title, wherever it sits in the page. */
  body:has(#twfNavChrome.visible) #pagesContainer .twf-dup-title { display: none; }
}

/* ── 2d. ONE tab strip ────────────────────────────────────────────────
   Every in-page tab bar is the same control now. Before this there were
   seven: the sticky #tabBarContainer, settings, cases, the shift hub,
   performance, the inbox, the time clock - each its own markup, each its
   own active-state idiom (a class, an inline colour, a swapped className),
   four of them a white bar with a 2px hairline. Two bars stacked on one
   screen was the "two inline navigations".

   All of them render <div class="tab-bar" role="tablist"> with
   <button class="tab-item"> children and toggle .active. This is their one
   skin, on every viewport: text on the canvas, 15px, the active word navy
   with a short gold rule under it, the rest muted. No fill, no hairline.

   A tab strip is for SIBLINGS - screens at the same level. Filters over
   one screen's data (this week / this month / all time) are not tabs; they
   are the segmented control below. */

.tab-bar {
  display: flex;
  gap: 18px;
  padding: 0 2px;
  margin: 0 0 8px;
  border-bottom: 0;
  background: none;
  overflow-x: auto;
  -webkit-overflow-scrolling: touch;
  scrollbar-width: none;
}
.tab-bar::-webkit-scrollbar { height: 0; }

.tab-bar .tab-item {
  flex-shrink: 0;
  border: 0;
  background: none;
  border-radius: 0;
  margin: 0;
  padding: 12px 0;
  min-height: 44px;
  font: inherit;
  font-size: 15px;
  font-weight: 600;
  color: var(--pt-mute, rgba(12, 18, 32, .55));
  position: relative;
  cursor: pointer;
  white-space: nowrap;
  -webkit-tap-highlight-color: transparent;
}

.tab-bar .tab-item:hover { color: var(--pt-navy, #0C1220); background: none; }

.tab-bar .tab-item.active {
  color: var(--pt-navy, #0C1220);
  background: none;
}

.tab-bar .tab-item.active::after {
  content: "";
  position: absolute;
  left: 0; right: 0; bottom: 6px;
  height: 2px;
  border-radius: 2px;
  background: var(--pt-gold, #C9A84C);
}

.tab-bar .tab-item:focus-visible {
  outline: 2px solid var(--pt-gold, #C9A84C);
  outline-offset: 2px;
}

/* The sticky container gets the header's blur and sits under the topbar. */
@media (max-width: 768px) {
  #tabBarContainer {
    top: calc(var(--twf-safe-top) + 58px);
    z-index: calc(var(--twf-z-header) - 2);
    background: color-mix(in srgb, var(--pt-canvas, #F5F6F8) 94%, transparent);
    backdrop-filter: saturate(180%) blur(20px);
    -webkit-backdrop-filter: saturate(180%) blur(20px);
  }
  #tabBarContainer .tab-bar { padding: 0 20px; margin: 0; }
}

/* ── 2e. The segmented control ───────────────────────────────────────
   iOS's control for "one of these windows over the same data". A quiet
   track, the chosen segment lifted white. 32px tall, 13px, because it is
   a filter and should read a step below the content it filters. Replaces
   the rows of gold/ghost .topbar-btn pills on the leaderboard, the peer
   board and performance. */

.twf-seg {
  display: inline-flex;
  gap: 2px;
  padding: 2px;
  border-radius: 10px;
  background: rgba(12, 18, 32, .06);
  max-width: 100%;
  overflow-x: auto;
  scrollbar-width: none;
}
.twf-seg::-webkit-scrollbar { height: 0; }

.twf-seg .twf-seg-btn {
  flex-shrink: 0;
  min-height: 32px;
  padding: 0 12px;
  border: 0;
  border-radius: 8px;
  background: none;
  font: inherit;
  font-size: 13px;
  font-weight: 600;
  color: var(--pt-mute, rgba(12, 18, 32, .55));
  cursor: pointer;
  white-space: nowrap;
  transition: background .15s, color .15s, box-shadow .15s;
  -webkit-tap-highlight-color: transparent;
}

.twf-seg .twf-seg-btn.active {
  background: #fff;
  color: var(--pt-navy, #0C1220);
  box-shadow: 0 1px 3px rgba(12, 18, 32, .12);
}

.twf-seg .twf-seg-btn:focus-visible {
  outline: 2px solid var(--pt-gold, #C9A84C);
  outline-offset: 2px;
}

/* ── 3. Content reserves the space the chrome occupies ───────────────── */

/* Bottom: the existing --mobile-chrome-height rule covers the main scroller.
   This catches the panels that scroll themselves, which otherwise end their
   last row underneath the thumb bar. */
.twf-page,
.twf-pt-page,
#pagesContainer {
  padding-bottom: calc(var(--mobile-chrome-height, 88px) + var(--twf-safe-bottom) + 8px);
}

/* Landscape and the notch side. Cheap, and it is the difference between
   text touching the curve of the screen and not. */
@media (orientation: landscape) {
  #pagesContainer,
  .twf-page {
    padding-left: max(var(--twf-safe-left), 12px);
    padding-right: max(var(--twf-safe-right), 12px);
  }
}

/* ── 4. Nothing outranks the header by accident ──────────────────────── */

/* The bottom nav is the one thing that legitimately sits above the header,
   because it is at the other end of the screen and must stay reachable. */
.twf-bottom-nav,
.twf-mobile-tray,
.twf-thumb-bar,
[data-twf-bottom-nav] {
  z-index: var(--twf-z-bottomnav);
}

/* ── 4b. Nothing lands on the thumb bar ──────────────────────────────────

   Reported 2026-09-09: "a horizontal bar covering the thumb nav at certain
   times."

   It was the toast. .toast-container was position:fixed, bottom:24px,
   right:24px, z-index:11000, with no mobile rule of any kind - a desktop
   convention that predates the phone-first build. On a 375px screen a toast
   is nearly edge to edge, so it reads as a bar, it lands squarely on the
   navigation, and because .toast has pointer-events:auto it also swallows
   taps on whatever tab is underneath for as long as it is up.

   "At certain times" was every time: confirming a volunteer, accepting a
   shift, saving anything. The most common feedback in the app was covering
   the most important control in it.

   Toasts sit above the chrome now, edge to edge with a margin, centred, and
   they still float bottom-right on a desktop where there is no thumb bar to
   cover. */
@media (max-width: 768px) {
  .toast-container {
    left: 12px;
    right: 12px;
    bottom: calc(var(--twf-chrome-bottom) + var(--twf-safe-bottom) + 12px);
    align-items: stretch;
  }
  .toast-container .toast { max-width: none; }
}

/* 2026-09-09: this used to drop the toast to the bottom edge while a sheet was
   open, on the reasoning that the sheet covers the thumb bar so there is
   nothing left to dodge. That was wrong in the obvious way: a bottom sheet
   occupies the bottom of the screen, so moving the toast down puts it on the
   sheet instead of on the nav. Traded one collision for another.

   A toast is a message about something that just happened; a sheet is the
   thing the person is looking at. The message goes to the other end of the
   screen and lets them read. */
@media (max-width: 768px) {
  body.twf-sheet-open .toast-container,
  body.twf-notif-open .toast-container {
    bottom: auto;
    top: calc(var(--twf-safe-top) + 12px);
  }
}

/* ── 5. iOS scrolling manners ────────────────────────────────────────── */

/* Momentum scrolling inside panels, and no rubber-banding the whole page
   when a panel reaches its end - the thing that makes a web app feel like a
   web page. */
#pagesContainer,
.twf-page,
.twf-pt-page {
  -webkit-overflow-scrolling: touch;
  overscroll-behavior-y: contain;
}
