/* ========================================
   Patient Hub primitives (brand-aligned)
   Light-grey body + white cards from the tehn.ca neutral stack,
   MGH lime/green accents, Montserrat display headings.
   ======================================== */

body.ios-mode {
  background: var(--ios-bg-body);
  color: var(--ios-ink);
}

/* The app shell owns horizontal gestures (swipe-nav.js). Without this,
   Chromium treats any unconsumed horizontal touch drag over the page as
   overscroll and can turn it into a history back/forward navigation -
   observed in the swipe audit as phantom navigations from drags over
   excluded surfaces (open dialogs, scrollers at their end). Root scroller
   only; inner scrollers keep their own behavior. Does not affect iOS
   Safari's bezel-edge history gesture.

   Vertically: contain (NOT none) - it disables Chrome Android's
   pull-to-refresh (a spinner flash plus a full reload that loses all state
   when the user tugs down at the top) and scroll chaining, but keeps the
   local elastic bounce/glow, so iOS rubber-banding still feels native. */
html {
  overscroll-behavior-x: none;
  overscroll-behavior-y: contain;
  /* The tab router (tab-bar.js tabScrollY) is the single owner of document
     scroll positioning: every landing restores the entering page's saved
     offset synchronously inside performSwap. CSS scroll anchoring fights
     that ownership: a swap hides one whole section subtree and unhides
     another (maximal anchor churn), and an engine-timed anchoring
     adjustment after our scrollTo paints a frame at a scroll position we
     did not set - the rAF re-scroll then yanks it back, which reads as a
     landing flicker. Anchoring is spec-suppressed while the scroller sits
     at block-start, which is exactly why pure swiping (all pages at
     scrollY 0) is clean and ONE small scroll on ONE page arms the flicker
     for subsequent swipes (reported on iPhone; WebKit ships anchoring
     since Safari 18.4; Chromium applies its adjustments in-task, which is
     why headless captures stay clean). components.css already excludes
     accordion internals from anchoring - this is the same opt-out, made
     total for the root scroller. */
  overflow-anchor: none;
}

/* In ios-mode: hide legacy accordion headers (the section is now a full screen)
   and force accordion-content to render regardless of its `hidden` attribute. */
.ios-mode .accordion-heading { display: none; }
.ios-mode .accordion-content[hidden] {
  display: block !important;
}
.ios-mode .accordion-content {
  padding: 0;
}
.ios-mode .home-section { background: var(--ios-bg-body); }
/* In iOS mode only one accordion-section is visible at a time (tab nav).
   Apply the same top margin to all of them so every subpage gets the same
   gap between the header bar and its first content (welcome card on Home,
   page hero on subpages) - matches Home's existing breathing room.
   Without this, only the DOM-first section (Home) got the gap via the
   `.accordion-section:first-child` rule in components.css. */
.ios-mode .accordion-section { margin-top: var(--space-sm); }
.ios-mode .site-footer {
  padding: 0.75rem 1rem;
  background: transparent;
  border-top: 1px solid var(--ios-border);
  font-size: 0.8125rem;
  color: var(--ios-muted);
  text-align: center;
  /* Clearance for the floating dock (~4rem tall) plus the bottom fade scrim
     above it (.dock-fade) plus safe-area for the iPhone home-indicator, so
     the last footer line comes to rest ABOVE the fade band rather than
     dissolving into it. Using env() makes the clearance adapt to devices
     with and without a home indicator. */
  margin-bottom: calc(env(safe-area-inset-bottom, 0px) + 6.5rem);
}
/* Last-reviewed date renders in the shared site footer on every tab
   (Home used to duplicate it - that block was removed from renderHome). */

/* Derived from --max-width, never hard-coded: the banners and the footer
   skyline resolve that same token, and a literal here is exactly how they came
   to be 198px wider than the column they sit in. See variables.css. */
.ios-mode .site-header,
.ios-mode .site-footer,
.ios-mode main#main-content {
  max-width: var(--max-width);
  margin-left: auto;
  margin-right: auto;
}

/* -------- Framed app column (large screens only) --------

   This is a phone-shaped app opened from a QR code at the bedside. On a desk
   monitor that column was simply marooned mid-screen against a flat grey field,
   reading as a page that had failed to load rather than a deliberate shape.

   Rather than invent a second, wider layout (a whole parallel surface to design,
   test and keep accessible), give the existing column a frame: the backdrop
   recedes, and the column reads as one lit, continuous surface.

   The header, main and footer are three separate siblings with no shared
   wrapper, so a single fixed pseudo-element paints the surface *behind* all
   three at once - no extra markup, no seams between them, and it stays put
   while content scrolls through it, the way a device would.

   Paint order this relies on (CSS 2.1 appendix E): html's background paints
   first, then negative-z-index children, then block descendants' backgrounds.
   So html carries the backdrop, body goes transparent to let the frame show,
   and every piece of content still paints above it.

   Phones and tablets never see any of this. */
@media (min-width: 1024px) {
  html {
    background: var(--app-backdrop);
  }

  body.ios-mode {
    background: transparent;
  }

  body.ios-mode::before {
    content: "";
    position: fixed;
    inset-block: 0;
    left: 50%;
    width: var(--max-width);
    transform: translateX(-50%);
    background: var(--ios-bg-body);
    box-shadow: var(--app-frame-shadow);
    z-index: -1;
    pointer-events: none;
  }
}

/* -------- Top bar (ED badge + label + EN + gear) --------
   Static app chrome: the bar is sticky, so pages scroll and slide UNDER it
   (the page-transition ghost and the swipe track both start at the bar's
   bottom edge and never paint over it). z-index keeps it above the page
   content but below every overlay: settings dialog (200), translate banner
   (500), map overlay (1000), dock (9001). */
.ios-top-bar {
  position: sticky;
  top: 0;
  z-index: 100;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-sm);
  /* The top padding reserves room for the fixed translate banner (0 when it
     is hidden), so the bar's visible content always clears the banner. */
  padding: calc(var(--space-sm) + var(--translate-banner-h, 0px)) var(--space-xs) var(--space-xs);
  background: var(--ios-bg-body);
  /* The scrolled hairline (below) can toggle for a single frame when an
     overscroll bounce-back overshoots the 2px data-scrolled threshold;
     fading it makes any transient flick imperceptible. utilities.css
     zeroes transition-duration under prefers-reduced-motion. */
  transition: box-shadow 200ms ease;
}

/* Hairline once content is scrolled beneath the bar (tab-bar.js flags the
   root); the bar's grey matches the page, so without this the content just
   vanishes at an invisible line. */
html[data-scrolled] .ios-top-bar {
  box-shadow: 0 1px 0 var(--ios-border);
}

.ios-brand {
  display: flex;
  align-items: center;
  gap: 0.625rem;
  /* Same 44px floor as the back pill so the bar keeps ONE height on every
     tab and at every text size - the transition ghost and swipe track are
     positioned off the bar's bottom edge, so a height change between tabs
     would open a sliver mid-slide. */
  min-height: 44px;
  text-decoration: none;
  color: inherit;
  border-radius: var(--border-radius-sm);
  /* Inline-start padding matches the card insets below. Cards sit at 16px
     from the viewport: 8px from `.accordion-body` padding + 8px from each
     card's own `var(--space-xs)` margin. Brand starts inside the top-bar's
     8px inline-start padding, so we add another 8px here to reach 16px.
     Logical properties so RTL languages (Arabic, Urdu, Hebrew) get the
     padding on the correct side. */
  padding-block: 0.25rem;
  padding-inline-start: var(--space-xs);
  padding-inline-end: 0.25rem;
}

.ios-brand:focus-visible {
  outline: var(--focus-outline);
  outline-offset: var(--focus-offset);
}

.ios-brand-label {
  display: flex;
  flex-direction: column;
  line-height: 1.15;
}

.ios-brand-kicker {
  font-family: var(--font-family-heading);
  /* Capped like the dock labels and back label: the bar is chrome with a
     fixed height budget (see .ios-brand min-height). Uncapped, the two brand
     lines outgrow the 44px floor at the largest text sizes and the bar's
     height would differ between Home (brand) and other tabs (44px pill). */
  font-size: min(var(--type-kicker), 14px);
  letter-spacing: var(--tracking-kicker);
  text-transform: uppercase;
  font-weight: 800;
  /* MGH brand green. Uses the deeper --ios-accent-strong (#33501C in light
     theme) instead of --ios-accent (#76B043) so the uppercase 13px small
     text still meets WCAG AAA contrast on the light-grey top bar. */
  color: var(--ios-accent-strong);
}

.ios-brand-subtitle {
  /* Capped - see .ios-brand-kicker. */
  font-size: min(0.9375rem, 16px);
  color: var(--ios-ink);
  font-weight: 500;
}

.ios-top-actions {
  display: flex;
  align-items: center;
  gap: 0.375rem;
}

.ios-pill {
  position: relative;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 0.375rem;
  min-height: 36px;
  min-width: 44px;
  padding: 0 0.75rem;
  border-radius: var(--radius-pill);
  background: var(--ios-bg-card);
  color: var(--ios-ink);
  font-size: 0.875rem;
  font-weight: 600;
  border: 1px solid var(--ios-border);
  text-decoration: none;
  cursor: pointer;
  transition: background var(--transition-fast), transform var(--transition-fast);
}

.ios-pill svg {
  width: 16px;
  height: 16px;
  stroke-width: 1.75;
}

.ios-pill:hover {
  background: var(--ios-bg-nested);
}

.ios-pill:focus-visible {
  outline: var(--focus-outline);
  outline-offset: var(--focus-offset);
}

.ios-pill--icon {
  width: 44px;
  height: 44px;
  min-width: 44px;
  padding: 0;
}

.ios-pill--icon svg {
  width: 20px;
  height: 20px;
}

/* -------- Kicker (all-caps section label) --------
   Visual + weight match `.stacked-card__kicker` on the Process page so
   section labels read consistently across Home, the hub pages, and any
   stacked-card layout. Strongly asymmetric margins (40px top, 4px bottom)
   so the kicker visually belongs to the content below it, not above.
   The action-grid / team-scroller / test-scroller blocks above the kicker
   carry significant visual weight (large card surfaces with shadows), so
   a 10:1 ratio is required for the kicker to read as part of the section
   below rather than a footer to the section above. */
.ios-kicker {
  font-size: var(--type-kicker);
  font-weight: 700;
  letter-spacing: var(--tracking-kicker);
  text-transform: uppercase;
  color: var(--ios-kicker);
  margin: 2.5rem 0 0.25rem;
  padding: 0 var(--space-xs);
}
/* When the kicker is the first child of its container (e.g. the first
   block on the Discharge & follow-up page), the page hero's padding
   already provides separation - no extra top margin needed. */
.ios-kicker:first-child {
  margin-top: 0;
}


/* -------- Welcome card / hero on Home -------- */
.ios-hero {
  background: var(--ios-bg-card);
  border: 1px solid var(--ios-border);
  border-radius: var(--radius-card);
  padding: 1.75rem 1.5rem;
  margin: 0 var(--space-xs) var(--space-md);
  box-shadow: var(--shadow-card);
}

.ios-hero__kicker {
  font-size: var(--type-kicker);
  letter-spacing: var(--tracking-kicker);
  text-transform: uppercase;
  color: var(--ios-kicker);
  font-weight: 600;
  margin: 0 0 0.5rem;
}

.ios-hero__title {
  font-family: var(--font-family-heading);
  font-size: var(--type-hero);
  line-height: 1.2;
  margin: 0 0 0.75rem;
  /* Brand display style: Montserrat 800 uppercase (tehn.ca signature) */
  font-weight: 800;
  text-transform: uppercase;
  letter-spacing: var(--tracking-display);
  color: var(--ios-ink);
}

.ios-hero__desc {
  font-size: var(--type-body);
  line-height: 1.45;
  color: var(--ios-muted);
  margin: 0 0 1.25rem;
}

.ios-hero__ctas {
  display: flex;
  flex-direction: column;
  gap: 0.625rem;
  align-items: flex-start;
}

.ios-btn {
  display: inline-flex;
  align-items: center;
  gap: 0.5rem;
  height: 48px;
  padding: 0 1.25rem;
  border-radius: var(--radius-pill);
  font-size: 1rem;
  font-weight: 600;
  text-decoration: none;
  border: 1px solid transparent;
  cursor: pointer;
  transition: transform var(--transition-fast), background var(--transition-fast);
}

.ios-btn:focus-visible {
  outline: var(--focus-outline);
  outline-offset: var(--focus-offset);
}

.ios-btn--primary {
  /* Brand primary button: lime surface + charcoal label (the tehn.ca
     pairing; white-on-lime fails AA). Works unchanged in dark mode. */
  background: var(--brand-primary);
  color: var(--text-on-brand);
  font-weight: 700;
}

.ios-btn--primary:hover {
  background: var(--brand-primary-light);
  transform: translateY(-1px);
}

[data-theme="dark"] .ios-btn--primary:hover {
  background: var(--brand-primary-light);
  color: var(--text-on-brand);
}

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

.ios-btn--ghost:hover {
  background: var(--ios-bg-nested);
}

.ios-btn svg {
  width: 18px;
  height: 18px;
  stroke-width: 1.75;
}

/* -------- Home tile list (single-column destination rows) --------
   POR Apr 28: Home uses a vertical list of "destination" rows (icon | title +
   subtitle stack | chevron) so it reads as a main menu and is visually
   distinct from the 2-col `.action-grid` on While You Wait. The shared
   `.ios-tile` class is overridden below for `.ios-tiles .ios-tile` only;
   `.action-grid .ios-tile` retains its base vertical-card layout from this
   file's `.ios-tile` rule below. */
.ios-tiles {
  display: flex;
  flex-direction: column;
  gap: 0.75rem;
  padding: 0 var(--space-xs);
  margin: 0 0 var(--space-md);
}

/* Override the base `.ios-tile { display: flex; flex-direction: column;
   min-height: 150px }` so Home tiles render as horizontal rows. Specificity
   `.ios-tiles .ios-tile` (0,2,0) beats `.ios-tile` (0,1,0). */
.ios-tiles .ios-tile {
  flex-direction: row;
  align-items: center;
  gap: 0.875rem;
  min-height: 0;
  padding: 0.875rem 1rem;
}

.ios-tiles .ios-tile__body {
  flex: 1;
  display: flex;
  flex-direction: column;
  gap: 0.125rem;
  min-width: 0;
}

.ios-tiles .ios-tile__title {
  margin: 0;
  font-size: 1.0625rem;
  line-height: 1.25;
}

.ios-tiles .ios-tile__subtitle {
  margin: 0;
  flex-grow: 0;
  font-size: 0.875rem;
}

/* Promote the chevron from absolute (where it'd anchor to the bottom-right
   corner of the row) into the flex flow as the trailing item, vertically
   centered next to the text body. */
.ios-tiles .ios-tile__chev {
  position: static;
  inset-inline-end: auto;
  inset-block-end: auto;
  flex-shrink: 0;
}

.ios-tile {
  background: var(--ios-bg-card);
  border: 1px solid var(--ios-border);
  border-radius: var(--radius-tile);
  padding: 1rem 1rem 0.875rem;
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
  min-height: 150px;
  text-decoration: none;
  color: inherit;
  cursor: pointer;
  transition: transform var(--transition-fast), box-shadow var(--transition-fast);
  position: relative;
}

.ios-tile:hover {
  box-shadow: var(--shadow-tab);
}

/* Lift on hover, gentle press on tap - app-like tactile feedback.
   Transforms sit behind the reduced-motion gate; the shadow change
   above still gives non-motion hover affordance. */
@media (prefers-reduced-motion: no-preference) {
  .ios-tile:hover {
    transform: translateY(-2px);
  }

  .ios-tile:active {
    transform: scale(0.98);
    box-shadow: var(--shadow-card);
  }

  .ios-pill:active {
    transform: scale(0.94);
  }

  /* The dock is the app's primary nav - every tap must be acknowledged the
     instant the finger lands, before the page slide even starts. */
  .ios-dock__link:active {
    transform: scale(0.92);
  }
}

.ios-tile:focus-visible {
  outline: var(--focus-outline);
  outline-offset: var(--focus-offset);
}

.ios-tile__icon {
  width: 36px;
  height: 36px;
  border-radius: var(--radius-check);
  background: var(--ios-accent-soft);
  color: var(--ios-accent);
  display: flex;
  align-items: center;
  justify-content: center;
  flex-shrink: 0;
}

.ios-tile__icon svg {
  width: 20px;
  height: 20px;
  stroke-width: 1.75;
}

.ios-tile__title {
  font-size: 1.125rem;
  font-weight: 700;
  color: var(--ios-ink);
  line-height: 1.2;
  margin: 0.25rem 0 0;
}

.ios-tile__subtitle {
  font-size: 0.9375rem;
  color: var(--ios-muted);
  margin: 0;
  flex-grow: 1;
}

.ios-tile__chev {
  position: absolute;
  inset-inline-end: 0.875rem;
  inset-block-end: 0.75rem;
  color: var(--ios-muted);
  opacity: 0.6;
}

/* -------- Feedback card -------- */
.ios-feedback {
  background: var(--ios-bg-card);
  border: 1px solid var(--ios-border);
  border-radius: var(--radius-card);
  padding: 1.25rem;
  margin: 0 var(--space-xs) var(--space-sm);
}

.ios-feedback__header {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  margin-bottom: 0.5rem;
}

.ios-feedback__header svg {
  width: 18px;
  height: 18px;
  color: var(--ios-accent);
}

.ios-feedback__title {
  font-size: var(--font-size-base);
  font-weight: 700;
  margin: 0;
  color: var(--ios-ink);
}

.ios-feedback__desc {
  font-size: var(--font-size-sm);
  color: var(--ios-muted);
  margin: 0 0 0.875rem;
  line-height: 1.4;
}

.ios-feedback__cta {
  display: flex;
  justify-content: center;
}

.ios-feedback__cta .ios-btn--primary {
  width: 100%;
  justify-content: center;
}

.ios-feedback__cta .ios-btn[aria-disabled="true"] {
  cursor: not-allowed;
  background: var(--ios-muted);
  opacity: 0.6;
}

.ios-feedback__divider {
  height: 1px;
  background: var(--ios-divider);
  margin: 1rem 0;
}

.ios-feedback__phone {
  font-size: 0.9375rem;
  color: var(--ios-muted);
  line-height: 1.5;
  margin: 0;
}

.ios-feedback__phone a {
  color: var(--ios-ink);
  font-weight: 600;
  text-decoration: none;
}

.ios-feedback__phone a:hover {
  text-decoration: underline;
}

/* "More information" link renders inline at the end of the phone paragraph.
   accent-strong, not accent: #76B043 measures 2.60:1 on the white card.
   Always underlined so the link is not distinguished by colour alone. */
.ios-feedback__more-info {
  color: var(--ios-accent-strong);
  font-weight: 600;
  text-decoration: underline;
  text-underline-offset: 2px;
  white-space: nowrap;
}

.ios-feedback__more-info:hover,
.ios-feedback__more-info:focus-visible {
  text-decoration-thickness: 2px;
}

.ios-feedback__more-info:focus-visible {
  outline: var(--focus-outline);
  outline-offset: var(--focus-offset);
  border-radius: 4px;
}

/* Screen-title headings (h1 with `data-screen-title`) are given `tabindex="-1"`
   and focused programmatically on tab switch so keyboard users resume below the
   title. They are not interactive, so suppress the generic focus-visible ring
   that would otherwise draw a green box around the title after each nav. */
.ios-mode [data-screen-title]:focus,
.ios-mode [data-screen-title]:focus-visible {
  outline: none;
}

/* -------- Bottom tab bar (dock) -------- */
.ios-dock {
  position: fixed;
  bottom: max(0.25rem, env(safe-area-inset-bottom));
  left: 50%;
  transform: translateX(-50%);
  /* Above the page-transition ghost / swipe track (z-index: 9000) AND the
     bottom fade scrim (z-index: 9001) so the dock stays sharp on top while
     the content behind it dissolves into the app background. */
  z-index: 9002;
  display: flex;
  /* The dock's GEOMETRY is fixed, in px, not rem - deliberately.
     It is a five-cell strip with a hard width budget, and its icons are
     already fixed at 22px. When the padding, gap and side margin were in rem
     they inflated with the text-size control, so raising text SHRANK every
     cell: "Journey" was already breaking to "Jour / ney" at the medium and
     large settings on a 320-360px phone, on the shipping site. Chrome that
     gets smaller as you ask for bigger text is backwards.
     Each value equals its old rem value at the default root size (2px / 6px /
     32px), so nothing moves for the vast majority of readers - it just stops
     collapsing for the ones who need larger text. */
  gap: 2px;
  padding: 6px;
  background: rgba(245, 246, 247, 0.92);
  border: 1px solid var(--ios-border);
  border-radius: var(--radius-pill);
  box-shadow: var(--shadow-tab);
  width: min(calc(100% - 32px), 370px);
  justify-content: space-between;
}

@supports (backdrop-filter: blur(10px)) {
  .ios-dock {
    background: rgba(245, 246, 247, 0.7);
    backdrop-filter: saturate(180%) blur(14px);
    -webkit-backdrop-filter: saturate(180%) blur(14px);
  }
}

[data-theme="dark"] .ios-dock {
  background: rgba(30, 30, 30, 0.88);
}

@supports (backdrop-filter: blur(10px)) {
  [data-theme="dark"] .ios-dock {
    background: rgba(30, 30, 30, 0.55);
  }
}

.ios-dock__link {
  flex: 1 1 0;
  min-width: 0;
  min-height: 52px;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  /* px for the same reason as .ios-dock above: fixed cell geometry. */
  gap: 2px;
  padding: 6px 4px;
  border-radius: var(--radius-pill);
  color: var(--ios-dock-muted);
  text-decoration: none;
  font-size: 0.75rem;
  font-weight: 500;
  transition: background var(--transition-fast), color var(--transition-fast),
    transform var(--transition-fast);
}

.ios-dock__link:hover {
  color: var(--ios-ink);
}

.ios-dock__link:focus-visible {
  outline: var(--focus-outline);
  outline-offset: -3px;
}

.ios-dock__link svg {
  width: 22px;
  height: 22px;
  stroke-width: 1.75;
}

/* Long translations (e.g. "En attendant" for Waiting) wrap to 2 lines.
   Clamp keeps the cell from overflowing if Google Translate produces a 3-line
   string in some language. */
.ios-dock__link span {
  display: -webkit-box;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 2;
  overflow: hidden;
  text-align: center;
  line-height: 1.1;
  word-break: break-word;
  /* Ceiling, like the display hero. The dock is a fixed-width 5-cell strip, so
     its labels have a hard budget no text-size setting can change; left to
     scale freely they hit the word-break above and read "Jour / ney", "Afte /
     r". Capped, they still grow (12px -> 15px) and stay whole words. The label
     is a landmark the patient recognises by shape - breaking it apart costs
     more legibility than the extra pixels buy. */
  font-size: min(0.75rem, 15px);
}

@media (max-width: 390px) {
  .ios-dock__link span {
    font-size: min(0.6875rem, 14px);
  }
}

/* -------- Dock over the open map modal --------

   The dock floats above the map overlay, and the map is a full-bleed,
   colour-coded floorplan. The dock's background is translucent (0.7 alpha
   where backdrop-filter is supported), so its 11px active label composites
   over whichever zone happens to sit behind it - measured at 3.33:1 against a
   green zone, below AA. Which zone that is depends on the map's zoom and pan,
   so the dock's contrast was never a property of the dock at all; it was
   chance. (Changing the map's default zoom is what surfaced it.)

   Going opaque over the modal makes contrast deterministic: 4.59:1 light,
   8.63:1 dark - exactly what the dock measures everywhere else on the site.
   The alphas were already 0.92/0.88, so this is imperceptible; the blur only
   ever had a colourful backdrop to work with here, which is the problem.

   Uses :has() (already relied on elsewhere in this codebase) because the dock
   precedes .map-overlay in the DOM, so no sibling combinator can reach it. */
body:has(.map-overlay:not([hidden])) .ios-dock {
  background: #F5F6F7;
  backdrop-filter: none;
  -webkit-backdrop-filter: none;
}

[data-theme="dark"] body:has(.map-overlay:not([hidden])) .ios-dock {
  background: #1E1E1E;
}

/* Bottom fade scrim. Page content dissolves into the app background as it
   reaches the dock, and nothing shows behind or below the dock. Fixed to the
   bottom, above the sliding layers (swipe track / page-transition ghost,
   z 9000) and below the dock (z 9002). The opaque lower band fully covers the
   dock so the dock's glass samples the background (nothing underneath); the
   gradient above it is the fade. Inert so it never intercepts a swipe. */
.dock-fade {
  position: fixed;
  left: 0;
  right: 0;
  bottom: 0;
  height: calc(env(safe-area-inset-bottom, 0px) + 6.25rem);
  z-index: 9001;
  pointer-events: none;
  /* Absolute stops (not percentages) so the solid band is ALWAYS exactly the
     dock's height + safe-area regardless of the device inset - it fully covers
     the dock (nothing shows behind it), then fades to transparent over the
     1.5rem above (tight fade: content stays sharp until close to the dock). */
  background: linear-gradient(
    to top,
    var(--ios-bg-body) calc(env(safe-area-inset-bottom, 0px) + 4.75rem),
    transparent
  );
}

/* The map overlay (z 1000) is a full-bleed surface below the fade; a bottom
   fade over it would just dim the floorplan. Hide the scrim while it is open
   (the dock goes opaque over the map via the rule above). */
body:has(.map-overlay:not([hidden])) .dock-fade {
  display: none;
}

/* The narrowest phones still in use (iPhone SE 1st gen, Galaxy Fold cover
   screen) leave each dock cell ~46px. "Journey" is the longest label and needs
   a 12.5px ceiling to survive there whole - measured, not estimated. The min()
   keeps the default size at 11px exactly as before; this only bites at the two
   largest text settings, where the alternative is "Jour / ney". */
@media (max-width: 340px) {
  .ios-dock__link span {
    font-size: min(0.6875rem, 12px);
  }
}

.ios-dock__link[aria-current="page"] {
  /* Soft lime pill: the active tab reads as a brand moment instead of a
     neutral grey. Label stays the dark accent green (4.8:1 over the
     lime-tinted glass surface). */
  background: var(--ios-accent-soft);
  color: var(--ios-accent-strong);
}

[data-theme="dark"] .ios-dock__link[aria-current="page"] {
  background: var(--ios-accent-soft);
  color: var(--ios-accent);
}

.ios-dock__link[data-sub-page="true"]::after {
  content: "";
  position: absolute;
  bottom: 3px;
  left: 50%;
  transform: translateX(-50%);
  width: 6px;
  height: 6px;
  border-radius: 50%;
  /* accent-strong at 6px, not lime at 4px: the lime dot measured 2.2-2.4:1
     against the dock surface and was near-invisible. A screen-reader
     equivalent is set alongside data-sub-page in tab-bar.js. */
  background: var(--ios-accent-strong);
}

.ios-dock__link {
  position: relative;
}

/* Dock clearance is handled by .site-footer margin-bottom; no extra
   padding on main (it only opens a gap before the footer). */

/* -------- In-header back affordance (non-Home tabs) -------- */
.ios-top-back {
  display: none;
  align-items: center;
  gap: 0.5rem;
  /* Height parity with .ios-brand - see the comment there. */
  min-height: 44px;
}

.ios-top-back .ios-pill--icon {
  background: var(--ios-bg-card);
}

/* Invisible hit-area extension: the pill renders 36px tall but must meet
   the project's 44px touch-target floor without inflating the top bar. */
.ios-pill::after {
  content: '';
  position: absolute;
  inset: -4px 0;
}

.ios-top-back__label {
  /* Capped for the same reason as the dock labels: this is chrome with a fixed
     budget. Uncapped, "After Your Visit" stacked to three lines and pushed the
     top bar down over the content at the largest text size. */
  font-size: min(1rem, 18px);
  font-weight: 600;
  color: var(--ios-ink);
  text-decoration: none;
  /* 44px touch-target floor: the text renders ~26px tall; stretch the
     link's hit area without changing the bar's visual height. */
  display: inline-flex;
  align-items: center;
  align-self: stretch;
  min-height: 44px;
}

.ios-top-back__label:hover {
  color: var(--ios-ink);
  text-decoration: underline;
}

.ios-top-back__label:focus-visible {
  outline: var(--focus-outline);
  outline-offset: var(--focus-offset);
  border-radius: var(--border-radius-sm);
}

/* Swap: brand shows on Home, back-affordance shows on every other tab.
   tab-bar.js sets body[data-current-tab] each time a tab activates. The bar
   itself is static chrome now (the transition ghost no longer clones it);
   the change between brand and back-affordance is animated by the title
   crossfade in tab-bar.js, whose outgoing clone carries inline display so
   these rules cannot hide it mid-fade. */
body:not([data-current-tab="home"]) .ios-brand {
  display: none;
}
body:not([data-current-tab="home"]) .ios-top-back {
  display: flex;
}

/* -------- Section hero (title + subtitle) -------- */
.ios-section-hero {
  /* Brand Book display band: uppercase Montserrat 800 charcoal on the
     lime #B2D235 field with a slim purple accent bar - the signature
     treatment from the MGH brochure covers and PowerPoint template.
     Deep-charcoal-on-lime (--text-on-brand) is 8.27:1 (AAA). Margins align
     the band's edges with the cards below (8px main padding + 8px card
     margin). */
  margin: 0 var(--space-xs) var(--space-sm);
  padding: var(--space-sm) var(--space-sm) 0.875rem;
  background: var(--brand-primary);
  border-radius: var(--radius-card);
  border-bottom: 4px solid var(--brand-purple);
}

.ios-section-hero__kicker {
  font-size: var(--type-kicker);
  font-weight: 600;
  letter-spacing: var(--tracking-kicker);
  text-transform: uppercase;
  color: var(--text-on-brand);
  margin: 0 0 0.375rem;
}

.ios-section-hero__title {
  font-family: var(--font-family-heading);
  /* Display type gets a ceiling; body copy does not.
     This is uppercase Montserrat 800 inside a fixed-width column, so it runs
     out of room far sooner than prose does: at the largest text size, the
     single word "PRESCRIPTIONS" rendered ~100px wider than its own box and put
     the whole page into horizontal scrolling. The vw term is what keeps it
     inside the column on a narrow phone; --type-hero still wins everywhere
     there is room (any viewport >=~300px at the default size, and every
     viewport at desktop), so the heading is unchanged for almost everyone.
     The reader still gets bigger *reading* text at every step - a heading is
     wayfinding, and mangling it does not help anyone read the page.
     base.css's overflow-wrap stays as the backstop, and `hyphens: auto` makes
     that backstop read as a deliberate break ("PRESCRIP-TIONS") rather than a
     glitch ("PRESCRIPTI / ONS"). <html lang> is set, so hyphenation has a
     dictionary on real devices (headless Chromium ships without one, so this
     looks like a no-op in screenshots - it is not).

     KNOWN LIMIT, accepted deliberately: on a 320px phone at the two largest
     text settings, "PRESCRIPTIONS" still wraps within itself. Only ~24px fits
     there, and pinning the cap that low (7.5vw) would shrink the heading for
     every *default* reader on a small phone - trading a common regression for a
     rare edge case. It already wrapped at the "large" setting before this
     change, at a larger size than it does now, so this is strictly an
     improvement; the layout itself never breaks either way. */
  font-size: min(var(--type-hero), 9vw);
  hyphens: auto;
  line-height: 1.15;
  /* Brand display style: Montserrat 800 uppercase (tehn.ca signature) */
  font-weight: 800;
  text-transform: uppercase;
  letter-spacing: var(--tracking-display);
  margin: 0 0 0.5rem;
  color: var(--text-on-brand);
}

.ios-section-hero__subtitle {
  font-size: var(--type-body);
  line-height: 1.45;
  /* Charcoal, not muted grey: --ios-muted is only 3.9:1 on the lime field. */
  color: var(--text-on-brand);
  margin: 0;
}

/* -------- Settings dialog: full-screen overlay + centered modal panel -------- */
.ios-settings-overlay {
  position: fixed;
  inset: 0;
  /* Above the dock (9001) - same modal-covers-navigation rule as the
     language modal (components.css). */
  z-index: 9300;
  display: flex;
  align-items: flex-start;
  justify-content: center;
  padding: 1rem;
  background: rgba(0, 0, 0, 0.18);
  -webkit-backdrop-filter: blur(2px);
  backdrop-filter: blur(2px);
  opacity: 0;
  transition: opacity 150ms ease-out;
}

.ios-settings-overlay.is-open {
  opacity: 1;
}

.ios-settings-overlay[hidden] { display: none; }

/* Keyed to [data-theme], not @media (prefers-color-scheme): the theme is
   seeded from the OS but the reader can override it in this very dialog. On
   the media query, someone on OS-dark who chose Light got a dark scrim behind
   a light panel (and the reverse). Every other overlay here already keys off
   [data-theme]. */
[data-theme="dark"] .ios-settings-overlay {
  background: rgba(0, 0, 0, 0.45);
}

.ios-gear-menu {
  width: 100%;
  max-width: 480px;
  max-height: calc(100vh - 2rem);
  /* dvh: iOS Safari's dynamic toolbar can clip the bottom of a 100vh box
     (same fallback pattern as .map-overlay in map.css). */
  max-height: calc(100dvh - 2rem);
  overflow-y: auto;
  background: var(--ios-bg-nested);
  border: 1px solid var(--ios-border);
  border-radius: var(--radius-tile);
  box-shadow: 0 24px 60px rgba(0, 0, 0, 0.28), 0 8px 16px rgba(0, 0, 0, 0.14);
  padding: 1rem;
  display: flex;
  flex-direction: column;
  gap: 0.375rem;
  transform: scale(0.98);
  transition: transform 150ms ease-out;
}

.ios-settings-overlay.is-open .ios-gear-menu {
  transform: scale(1);
}

.ios-settings-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin-bottom: 0.25rem;
}

.ios-settings-title {
  margin: 0;
  font-size: 1.125rem;
  font-weight: 600;
  color: var(--ios-ink);
  letter-spacing: -0.01em;
}

.ios-settings-close {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 44px;
  height: 44px;
  margin: -0.5rem -0.5rem 0 0;
  padding: 0;
  background: transparent;
  border: none;
  border-radius: 50%;
  color: var(--ios-ink);
  cursor: pointer;
}

.ios-settings-close:hover {
  background: var(--ios-bg-nested);
}

.ios-settings-close:focus-visible {
  /* The global ring, not the lime accent: #76B043 measures 2.35:1 against
     the gear menu's #F3F3F3 surface (WCAG 1.4.11 needs 3:1). */
  outline: var(--focus-outline);
  outline-offset: 2px;
}

.ios-settings-close svg {
  width: 22px;
  height: 22px;
}

@media (prefers-reduced-motion: reduce) {
  .ios-settings-overlay,
  .ios-gear-menu {
    transition: none;
  }
}

/* Removed: .ios-gear-section - no element carries it. */

.ios-gear-title {
  font-size: var(--type-kicker);
  letter-spacing: var(--tracking-kicker);
  text-transform: uppercase;
  color: var(--ios-kicker);
  font-weight: 600;
  margin: 0;
}

.ios-gear-title--spaced {
  margin-top: 1.25rem;
}

.ios-gear-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.5rem;
  padding: 0.5rem 0.75rem;
  border-radius: var(--border-radius-sm);
  background: transparent;
  border: 1px solid var(--ios-border);
  color: var(--ios-ink);
  font-size: 0.9375rem;
  cursor: pointer;
  min-height: 44px;
}

.ios-gear-row:hover {
  background: var(--ios-bg-nested);
}

/* -------- Section body content inset (visual parity with cards) -------- */
.ios-mode .accordion-body {
  padding: 0 var(--space-xs);
}

/* -------- Helper for hidden but kept-in-DOM elements -------- */
/* Removed: .ios-hidden - no element carries it (and it was the file's only
   unjustified !important). */

/* -------- Respect prefers-reduced-motion -------- */
@media (prefers-reduced-motion: reduce) {
  .ios-tile,
  .ios-btn--primary,
  .ios-pill {
    transition: none;
  }
}

/* -------- Page transitions: iOS-style carousel slide for tab navigation.
   The ghost is a position:fixed clone of the leaving SECTION only - the
   top bar is sticky chrome and never slides. The ghost starts at the bar's
   bottom edge (JS sets style.top) and slides off while the live section
   slides in underneath the static bar. The slide itself is driven by the
   Web Animations API in tab-bar.js (runPageTransition); these rules only
   set up the ghost's chrome (positioning, z-index, depth shadow).
   Reduced-motion is handled globally in utilities.css; the JS guard in
   runPageTransition() also skips ghost cloning entirely when the user has
   it on. -------- */

/* During a carousel the entering page is translated a full viewport-width to
   one side before it slides in, which extends the document's scroll width and
   would flash a horizontal scrollbar (and let the page be dragged into empty
   space) for the ~300ms of the slide. Clip it, scoped to the transition via
   the attribute JS already sets on <html>, so it is gone the instant the slide
   ends. `clip`, not `hidden`: it does not establish a scroll container, so it
   cannot disturb the vertical scroll position (the code deliberately never
   locks body.overflow for that reason - see runPageTransition). */
html[data-page-transition] {
  overflow-x: clip;
}

/* While a page slides, lift the fixed translate banner above the ghost (z 9000)
   so the pages travel underneath it. Scoped to the transition so the banner
   otherwise stays below the map overlay (z 1000) and the map still covers it. */
html[data-page-transition] .translate-disclaimer {
  z-index: 9100;
}

.page-transition-ghost {
  position: fixed;
  /* JS overrides `top` inline with the sticky bar's bottom edge, so the
     ghost slides under the bar instead of over it. */
  inset: 0;
  z-index: 9000;
  overflow: hidden;
  pointer-events: none;
  /* Matches the app body, not --bg (white): in the carousel this panel slides
     fully across the screen, so its backing must be the same grey the pages sit
     on, or the inter-card gutters and any margin the slide reveals would flash
     white. */
  background: var(--ios-bg-body);
  contain: layout paint;
  will-change: transform;
  /* JS sets the directional shadow inline (box-shadow with a +/- offset
     depending on direction and RTL) so the side facing the underlying
     live page picks up the depth cue. */
}

.page-transition-ghost__inner,
.swipe-panel__inner {
  /* JS sets transform: translateY(...) inline so the clone lines up
     pixel-for-pixel with where the section sits (or would sit) on screen.
     The column constraint mirrors main#main-content EXACTLY - max-width,
     centering AND the inline padding from base.css. Any mismatch renders
     the clone at a different content width than the live section, so text
     wraps differently and the page jumps when the overlay lifts. */
  width: 100%;
  max-width: var(--max-width);
  margin-inline: auto;
  padding-inline: var(--space-xs);
}

/* -------- Swipe navigation (swipe-nav.js) --------
   The finger drags a fixed two-panel filmstrip of section clones: the
   current page and the swipe target sit side by side and travel together,
   below the static top bar (JS sets `top`) and under the dock (9001). */
html[data-swipe] {
  /* Same reasoning as html[data-page-transition] above: a rubber-band drag
     translates the live section sideways and must not open a horizontal
     scrollbar. clip, not hidden - no new scroll container. */
  overflow-x: clip;
}

.swipe-track {
  position: fixed;
  left: 0;
  right: 0;
  bottom: 0;
  z-index: 9000;
  pointer-events: none;
  will-change: transform;
  /* The track must never clip its own box - not via overflow, not via a
     clipping path. The target panel waits at translate3d(+/-100%),
     entirely outside the track's border box: an overflow clip removed it
     from painting (f9bf3d1 regression), and a negative-extent inset path
     fixed Chromium but iOS Safari does not honor the extension - the live
     page bled in at the screen edges mid-flight (frame-stepped device
     recording). There is no seam depth cue any more (the two pages ride
     edge to edge as one carousel, Alvin 2026-07-23), so there is nothing
     that could spill and nothing to clip. Pinned by
     tests/browser/swipe-preview.test.mjs and swipe-nav.test.mjs. */
}

.swipe-panel {
  position: absolute;
  inset: 0;
  overflow: hidden;
  /* Same backing as the transition ghost, same reason. */
  background: var(--ios-bg-body);
  contain: layout paint;
}

/* Wrapper for the skyline + footer clones a ghost/panel carries
   (cloneFooterBand in tab-bar.js). The live band's elements are direct
   children of body and size themselves against the full shell width; the
   ghost/panel inners re-apply the main-column padding for the section
   clones, so without this escape the cloned band renders 2x the column
   padding narrower - the skyline buildings rescale, its auto height drops
   ~1px, and the swipe landing fade crossfades between two visibly
   different footer bands. */
.clone-footer-band {
  margin-inline: calc(-1 * var(--space-xs));
}

/* Stand-in for iframes inside section clones: a cloned iframe is a fresh
   browsing context (blank + a network load mid-gesture), so tab-bar.js
   swaps it for this block. The .map-embed-wrapper box supplies the size. */
.clone-iframe-placeholder {
  position: absolute;
  inset: 0;
  background: var(--ios-bg-nested);
}

/* Anchor jumps and programmatic scrolls must clear the sticky bar.
   --ios-top-bar-h is published by tab-bar.js from the bar's rendered
   height. Overrides the pre-sticky 16px scroll-margin that components.css
   puts on .accordion-header. */
.ios-mode main#main-content :is([id], details, summary, h1, h2, h3, h4, .accordion-header, .zone-btn) {
  scroll-margin-top: calc(var(--ios-top-bar-h, 64px) + var(--space-xs));
}

/* ========================================
   Brand typography (MGH Brand Book / tehn.ca)
   Montserrat carries every card title and all-caps kicker so the
   heading voice is consistent from the page hero down to tiles.
   Titles stay sentence case at 700; the uppercase display treatment
   is reserved for h1/h2 + heroes (see base.css and the hero rules
   above) so long question-style titles keep grade 6-8 readability.
   ======================================== */
.ios-tile__title,
.ios-feedback__title,
.ios-settings-title,
.language-modal-title,
.feature-card__title,
.team-tile__title,
.test-tile__title,
.list-card__title,
.dodont__head {
  font-family: var(--font-family-heading);
}

.ios-kicker,
.ios-hero__kicker,
.ios-section-hero__kicker,
.feature-card__kicker,
.ios-gear-title,
.ios-mode .stacked-card__kicker,
.expanded-card h4 {
  font-family: var(--font-family-heading);
}
