/* ===========================================================================
   USMLE PULSE — the mobile layer of the design system

   WHAT THIS IS NOT
   Not a second design system. The tokens in main.css — surfaces, content,
   typography, radius, shadow, motion, and a dark mode that redefines the
   semantic layer rather than inverting colours — are the design system, and
   they are good. This adds only what a phone needs and the desktop never did.

   HOW IT CANNOT BREAK THE WEBSITE
   Everything that CHANGES layout is scoped to `html.pulse-native`, a class
   added by assets/js/lib/native.js only when Capacitor is running. A browser
   never has it, so a browser never sees these rules. What is defined globally
   is only tokens, and a token nothing reads changes nothing.

   That scoping is asserted by tools/test-design-system.mjs, so it cannot be
   loosened by accident later.
   =========================================================================== */

/* ---------------------------------------------------------------------------
   1. THE EDGES OF THE SCREEN

   A phone's screen is not the area you may draw in. A notch, a Dynamic Island,
   a home indicator and Android's gesture bar all overlap it, and the amount
   they overlap by is only known at runtime. env() reports it; the fallback of
   0px means these are safe to use everywhere, including on the desktop where
   they resolve to nothing.

   Defined on :root rather than under .pulse-native because mobile Safari has
   a notch too, and a page that wants to respect it should not have to be in
   the app to ask.
   --------------------------------------------------------------------------- */
:root {
  --safe-t: env(safe-area-inset-top, 0px);
  --safe-r: env(safe-area-inset-right, 0px);
  --safe-b: env(safe-area-inset-bottom, 0px);
  --safe-l: env(safe-area-inset-left, 0px);

  /* ---- the smallest thing a finger can reliably hit ----------------------
     44px is Apple's floor, 48dp is Google's. 48 satisfies both, so there is
     one number rather than two and no argument about which platform a
     component is for. A control smaller than this is a control that gets
     mis-tapped, and mis-taps in a study app cost a grade. */
  --tap-min: 48px;

  /* ---- mobile chrome ---------------------------------------------------- */
  --nav-h: 56px;            /* the tab bar itself, above the safe area */
  --sheet-r: 20px;          /* a sheet's top corners */
  --sheet-grab: 36px;       /* the drag handle */

  /* ---- how far content sits from the edge on a small screen -------------- */
  --gutter: 1rem;
  --gutter-lg: 1.25rem;
}

/* ===========================================================================
   EVERYTHING BELOW APPLIES INSIDE THE APP ONLY
   =========================================================================== */

/* ---------------------------------------------------------------------------
   2. THE APP IS NOT A WEB PAGE

   Three habits of a browser that read as broken in an app: the rubber-band
   overscroll that shows a white void behind the page, the blue tap highlight
   on every touch, and text that a long-press offers to select and copy.
   --------------------------------------------------------------------------- */
html.pulse-native {
  overscroll-behavior-y: none;
  -webkit-tap-highlight-color: transparent;
  /* A phone rotating or a keyboard opening must not resize the world. */
  height: 100%;
}

html.pulse-native body {
  /* Painted behind the safe areas, so the notch and the gesture bar sit on
     the app's own colour rather than on white. */
  background: var(--surface-page, var(--paper));
  padding-left: var(--safe-l);
  padding-right: var(--safe-r);
  overscroll-behavior-y: none;
}

/* Reading matter stays selectable — students quote answers. Chrome does not. */
html.pulse-native .site-header,
html.pulse-native .pulse-tabbar,
html.pulse-native .btn,
html.pulse-native button {
  -webkit-user-select: none;
  user-select: none;
}

/* ---------------------------------------------------------------------------
   3. NOTHING SMALLER THAN A FINGERTIP
   --------------------------------------------------------------------------- */
html.pulse-native .btn,
html.pulse-native button,
html.pulse-native .input,
html.pulse-native select,
html.pulse-native [role="button"] {
  min-height: var(--tap-min);
}

/* A checkbox is the exception that proves the rule: it stays its own size, and
   the LABEL around it grows to catch the tap. Growing the box itself would
   make every task list look like a form. */
html.pulse-native label:has(> input[type="checkbox"]),
html.pulse-native label:has(> input[type="radio"]) {
  min-height: var(--tap-min);
  display: flex;
  align-items: center;
}

/* iOS zooms the whole page when a field smaller than 16px takes focus, and
   never zooms back out. This is the fix, and it is the only reason these
   sizes are pinned. */
html.pulse-native input,
html.pulse-native select,
html.pulse-native textarea {
  font-size: max(16px, 1rem);
}

/* ---------------------------------------------------------------------------
   4. THE TAB BAR

   Home · Plan · Pomodoro · Messages · Profile — the five things a student does.
   Fixed to the bottom because that is where a thumb is, not the top where a
   desktop navigation would be.
   --------------------------------------------------------------------------- */
.pulse-tabbar {
  position: fixed;
  left: 0; right: 0; bottom: 0;
  z-index: 500;
  display: none;                       /* the website never shows it */
  align-items: stretch;
  height: calc(var(--nav-h) + var(--safe-b));
  padding-bottom: var(--safe-b);       /* the bar's own height stays --nav-h */
  padding-left: var(--safe-l);
  padding-right: var(--safe-r);
  /* A MATERIAL, NOT A STRIP.

     This was `background: var(--surface-raised)` with `border-top: 1px solid`,
     which is an opaque band with a rule drawn across the top. Content was
     already scrolling behind it — the bar is fixed and the body's
     padding-bottom is what keeps the last list item reachable — so the only
     thing the opacity achieved was hiding that fact.

     color-mix on the token the bar already used, so dark mode needs no second
     value and a palette change reaches this automatically. Same construction
     as .site-header. */
  background: color-mix(in srgb, var(--surface-raised, var(--paper-2)) 78%, transparent);
  -webkit-backdrop-filter: saturate(180%) blur(20px);
  backdrop-filter: saturate(180%) blur(20px);

  /* Light catching the top edge of glass, rather than a border between two
     regions. currentColor at 10% follows the theme without a second token. */
  border-top: 1px solid color-mix(in srgb, var(--line) 55%, transparent);

  /* Rests above the keyboard rather than being pushed off-screen by it. */
  transform: translateZ(0);
}

/* WHERE CONTENT MEETS THE CHROME.

   A 1px rule says "two regions". A short fade says "this floats over that",
   which is what is actually happening. Sits directly above the bar and moves
   with it, so it is correct while the keyboard is open too.

   pointer-events:none is not optional: this covers the bottom 20px of the
   scrolling area, and a decorative layer that swallows taps there would be
   blamed on the list, the router, anything but a gradient. */
.pulse-tabbar::before {
  content: "";
  position: absolute;
  left: 0; right: 0;
  bottom: 100%;
  height: 20px;
  pointer-events: none;
  background: linear-gradient(
    to top,
    color-mix(in srgb, var(--surface-raised, var(--paper-2)) 55%, transparent),
    transparent);
}
html.pulse-native .pulse-tabbar { display: flex; }

.pulse-tab {
  flex: 1 1 0;
  display: grid;
  place-items: center;
  gap: 2px;
  padding: 0;
  border: 0;
  background: none;
  font: inherit;
  font-size: .66rem;
  font-weight: 600;
  letter-spacing: .01em;
  color: var(--content-muted, var(--text-muted));
  text-decoration: none;
  min-height: var(--nav-h);
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
}
.pulse-tab svg,
.pulse-tab .pulse-tab-ic { width: 22px; height: 22px; display: block; }

.pulse-tab[aria-current="page"],
.pulse-tab.is-on { color: var(--content-brand, var(--brand-text)); }

/* The pressed state is a background, not a transform: a tab bar that moves
   under the thumb reads as a mis-tap. */
.pulse-tab:active { background: var(--surface-hover, rgba(12,26,36,.04)); }

/* THE MARKER THAT MOVES BETWEEN TABS.

   Without it the bar's only answer to a navigation is that a different tab is
   a different colour on the next page — a state that changed, not a thing that
   happened. This slides.

   One tab wide and moved by whole multiples of itself, because every .pulse-tab
   is flex:1 1 0 and therefore exactly equal. transform only: no width to
   animate, no layout to recompute, and the compositor can carry it while the
   next document is being parsed.

   The visible pill is the ::before, narrower than the marker that carries it,
   so the travel is measured in tab widths while the thing on screen is the
   width of an icon. */
.pulse-tab-ind {
  position: absolute;
  top: 0;
  left: 0;
  height: 2px;
  pointer-events: none;
  will-change: transform;
}
.pulse-tab-ind::before {
  content: "";
  display: block;
  width: 22px;
  height: 100%;
  margin: 0 auto;
  border-radius: 0 0 2px 2px;
  background: var(--content-brand, var(--brand-text));
}

/* .is-moving is added by tabbar.js AFTER the first placement, so a cold start
   puts the marker where it belongs instead of sliding it in from the left edge
   of the screen. Everything after that animates. */
.pulse-tab-ind.is-moving {
  transition: transform .42s cubic-bezier(.32, .72, 0, 1);
}
.pulse-tab-ind[hidden] { display: none; }

/* Reduced motion does not mean no marker — it means the marker arrives without
   travelling. tabbar.js also skips the from-position entirely, so this is the
   second of two independent guards rather than a restatement of one. */
@media (prefers-reduced-motion: reduce) {
  .pulse-tab-ind.is-moving { transition: none; }
}

/* Room for the bar, so the last task in a list is not sitting under it. */
html.pulse-native body { padding-bottom: calc(var(--nav-h) + var(--safe-b)); }

/* THE SAME ROOM, FOR THE THINGS THAT DO NOT SCROLL.

   body padding moves content. It does nothing for position:fixed, and the
   messaging button is fixed: main.css puts .chat-fab at bottom:20px with
   z-index:150, and the bar is 56px tall plus the safe area at z-index:500.
   Measured on a 375×812 phone, 42 of the button's 56 pixels were behind the
   bar and the rest was painted underneath it. It was not awkward to hit — it
   was unreachable.

   BOTH HALVES HAVE TO CHANGE. Clearing the bar's height is what removes the
   overlap; clearing its z-index is what stops the 20px fade above the bar
   tinting a button that is now sitting in it. Fixing one and not the other
   looks fixed in a screenshot and is not.

   The panel is capped as well as moved: its height in main.css is
   calc(100vh - 140px), which — once the whole thing is lifted a bar's height
   further up — puts its top edge off the top of the screen on a small phone. */
html.pulse-native .chat-fab {
  bottom: calc(var(--nav-h) + var(--safe-b) + 16px);
  z-index: 520;
}
html.pulse-native .chat-panel {
  bottom: calc(var(--nav-h) + var(--safe-b) + 76px);
  height: min(520px, calc(100vh - var(--nav-h) - var(--safe-b) - var(--safe-t) - 150px));
  z-index: 520;
}

/* ---------------------------------------------------------------------------
   5. SHEETS

   A phone's answer to a dialog. Rises from the bottom, within thumb reach,
   and can be dismissed by pushing it back down.
   --------------------------------------------------------------------------- */
.pulse-sheet {
  position: fixed;
  inset: 0;
  z-index: 700;
  display: flex;
  align-items: flex-end;
  background: rgba(12, 21, 28, .55);
  -webkit-backdrop-filter: blur(2px);
  backdrop-filter: blur(2px);
}
.pulse-sheet-box {
  width: 100%;
  max-height: 88vh;
  overflow-y: auto;
  -webkit-overflow-scrolling: touch;
  overscroll-behavior: contain;        /* scrolling the sheet must not scroll the page */
  background: var(--surface-overlay, var(--paper-2));
  color: var(--content-primary, var(--text));
  border-radius: var(--sheet-r) var(--sheet-r) 0 0;
  padding: .6rem var(--gutter) calc(var(--gutter) + var(--safe-b));
  box-shadow: var(--shadow-lg);
  animation: pulse-sheet-up var(--dur, .28s) var(--ease-out, cubic-bezier(.16,1,.3,1));
}
.pulse-sheet-grab {
  width: var(--sheet-grab);
  height: 4px;
  margin: 0 auto .8rem;
  border-radius: 999px;
  background: var(--line);
}
@keyframes pulse-sheet-up {
  from { transform: translateY(12%); opacity: .6; }
  to   { transform: none; opacity: 1; }
}

/* ---------------------------------------------------------------------------
   6. SKELETONS

   Section 40: every asynchronous action needs a loading state, and a blank
   screen is not one. A skeleton says "something is coming and it will be
   shaped like this", which a spinner does not.
   --------------------------------------------------------------------------- */
.pulse-skel {
  border-radius: var(--r-sm, 8px);
  background: linear-gradient(90deg,
              var(--surface-sunken, rgba(12,26,36,.06)) 25%,
              var(--surface-hover, rgba(12,26,36,.02)) 37%,
              var(--surface-sunken, rgba(12,26,36,.06)) 63%);
  background-size: 400% 100%;
  animation: pulse-skel 1.4s ease-in-out infinite;
}
.pulse-skel-line { height: 1em; margin: .35em 0; }
.pulse-skel-line.is-short { width: 55%; }
@keyframes pulse-skel { from { background-position: 100% 50%; } to { background-position: 0 50%; } }

/* ---------------------------------------------------------------------------
   7. ACCESSIBILITY

   Section 41. Reduce Motion is a request, not a preference to weigh against
   the design: everything that moves stops. The tab bar and sheets still work;
   they simply appear.
   --------------------------------------------------------------------------- */
@media (prefers-reduced-motion: reduce) {
  .pulse-sheet-box { animation: none; }
  .pulse-skel { animation: none; }
}

/* Keyboard and switch-control users need to see where they are. The website
   has its own focus styles; this only guarantees the mobile components have
   one too. */
.pulse-tab:focus-visible,
.pulse-sheet-box :focus-visible {
  outline: 2px solid var(--brand);
  outline-offset: 2px;
}

/* Larger text must not break the tab bar into two lines. */
@media (min-resolution: 0.001dppx) {
  .pulse-tab { line-height: 1.1; }
}

/* ---------------------------------------------------------------------------
   8. A NOTIFICATION THAT ARRIVED WITH THE APP OPEN

   Android shows nothing in the tray while the app is in the foreground, so
   the page has to show it — and it has to be actionable, because the same
   message tapped from the tray takes the student somewhere. A toast that
   informs and a toast that navigates are different things: this one is a
   button, it is reachable from a keyboard, and it says where it goes.

   Below the safe area at the TOP rather than the bottom: the bottom is where
   the tab bar and the thumb are, and a banner that covers the control you
   were reaching for is how a mis-tap happens.
   --------------------------------------------------------------------------- */
html.pulse-native .pulse-push-toast {
  position: fixed;
  top: calc(var(--safe-t) + .5rem);
  left: calc(var(--safe-l) + .5rem);
  right: calc(var(--safe-r) + .5rem);
  z-index: 600;
  display: flex;
  align-items: stretch;
  gap: .25rem;
  border-radius: var(--r-md, 14px);
  background: var(--surface-raised, var(--paper-2));
  border: 1px solid var(--line);
  box-shadow: var(--shadow-lg);
  overflow: hidden;
  animation: pulse-push-in var(--dur, .28s) var(--ease-out, cubic-bezier(.16,1,.3,1));
}
html.pulse-native .pulse-push-toast-btn {
  flex: 1 1 auto;
  display: flex; align-items: center; gap: .6rem;
  min-height: var(--tap-min, 48px);
  padding: .7rem .9rem;
  border: 0; background: none;
  font: inherit; font-size: .92rem; line-height: 1.4;
  color: var(--content-primary, var(--text));
  text-align: left;
  cursor: pointer;
}
html.pulse-native .pulse-push-toast-btn:active { background: var(--surface-hover, rgba(12,26,36,.05)); }
html.pulse-native .pulse-push-toast-go { margin-left: auto; color: var(--content-brand, var(--brand-text)); font-size: 1.1rem; }
html.pulse-native .pulse-push-toast-x {
  flex: none;
  width: var(--tap-min, 48px);
  border: 0; border-left: 1px solid var(--line);
  background: none; color: var(--content-muted, var(--text-muted));
  font-size: .9rem; cursor: pointer;
}
html.pulse-native .pulse-push-toast-btn:focus-visible,
html.pulse-native .pulse-push-toast-x:focus-visible { outline: 2px solid var(--brand); outline-offset: -3px; }

/* THE RESTING STATE IS THE VISIBLE STATE.

   This keyframe used to start at opacity: 0, and the banner was invisible
   wherever the animation did not actually run — which is not a hypothetical:
   PHASE3-REPORT.md records the same bug in .reveal, where an IntersectionObserver
   that never fired left content permanently hidden in an in-app web view. An
   element that depends on an animation to become visible has two ways to
   appear and one way to disappear.

   So it only moves. If the animation never runs, or is cut off by
   prefers-reduced-motion, or the web view throttles it, the banner is simply
   there — which for a notification is the only acceptable failure. */
@keyframes pulse-push-in { from { transform: translateY(-10px); } to { transform: none; } }
@media (prefers-reduced-motion: reduce) { html.pulse-native .pulse-push-toast { animation: none; } }

/* ---------------------------------------------------------------------------
   10. THE TWO READERS

   read-notes and all-chapters were exempt from the tab bar until now, on the
   grounds that a fixed bottom bar would fight the chrome they already have.
   That was true and it was not a reason to leave two of the app's pages
   without navigation — it was a reason to make room.

   read-notes floats its annotation toolbar at the bottom of the screen. It
   now sits above the tab bar rather than under it, which is also the right
   order: the pen belongs nearer the page than the navigation does.
   --------------------------------------------------------------------------- */
html.pulse-native .annot-bar {
  bottom: calc(var(--nav-h) + var(--safe-b) + 12px);
}
/* The reader fills the screen, so its own scroller needs the room rather than
   the body — padding on the body would move the toolbar, not the pages. */
html.pulse-native .reader-scroll,
html.pulse-native .reader-stage {
  padding-bottom: calc(var(--nav-h) + var(--safe-b));
}

/* ---------------------------------------------------------------------------
   9. THE GARDEN, ON A PHONE

   §19: do not shrink the desktop garden onto a phone. Two things here are not
   about size.

   THE PAGE HAS TO SCROLL PAST IT. The scene is as wide as the screen, so a
   thumb landing on a plant is far more often the start of a scroll than the
   start of an arrangement. touch-action says so to the browser before any
   script runs — pan-y while browsing, and lib/garden-mobile.js switches it to
   none only while Edit is on, so a drag is not fighting the page.

   AND IT DESERVES THE HEIGHT. A 500x408 scene in a portrait viewport renders
   as a short band with the student's whole garden squeezed into it. A little
   more room, and the horizon actually reads as one.
   --------------------------------------------------------------------------- */
html.pulse-native .garden,
html.pulse-native [data-garden-mount] {
  touch-action: pan-y;
}

html.pulse-native .pg-svg {
  /* Tall enough to be a place rather than a strip, and capped so it never
     eats the whole screen on a short handset. */
  min-height: min(46vh, 340px);
}


/* ---------------------------------------------------------------------------
   11. APPLE GUIDELINE 3.1.1

   native.js REMOVES these from the DOM; this only stops one frame of a
   purchase button being painted before the script runs. Scoped to the app,
   so the website — where subscriptions are actually bought — is untouched.
   --------------------------------------------------------------------------- */
html.pulse-native [data-web-purchase] { display: none !important; }

/* ===========================================================================
   ACCESSIBILITY FALLBACKS FOR THE APP'S SHEET SCRIM

   backdrop-filter is a visual effect, not a background. With transparency
   reduced the blur is what has to go, and something opaque has to take its
   place — otherwise the surface simply becomes a flat wash over whatever is
   behind it, which is worse than either.

   prefers-contrast: more additionally wants a defined edge: a floating panel
   whose boundary is implied by a blur has no boundary at all once the blur is
   gone.
   =========================================================================== */
@media (prefers-reduced-transparency: reduce) {
  /* A scrim exists to push the page back. Opaque does that better, not worse. */
  .pulse-sheet {
    background: rgba(12, 21, 28, .94);
    backdrop-filter: none;
    -webkit-backdrop-filter: none;
  }

  /* The bar goes back to being a solid strip, and the fade above it goes with
     the blur — a gradient with nothing translucent under it is just a smudge. */
  .pulse-tabbar {
    background: var(--surface-raised, var(--paper-2));
    backdrop-filter: none;
    -webkit-backdrop-filter: none;
    border-top: 1px solid var(--line);
  }
  .pulse-tabbar::before { display: none; }
}
@media (prefers-contrast: more) {
  .pulse-sheet { background: rgba(12, 21, 28, .97); backdrop-filter: none; -webkit-backdrop-filter: none; }

  /* Here the hard divider is the RIGHT answer: with contrast turned up the
     boundary between content and chrome should be stated, not suggested. */
  .pulse-tabbar {
    background: var(--surface-raised, var(--paper-2));
    backdrop-filter: none;
    -webkit-backdrop-filter: none;
    border-top: 2px solid var(--text);
  }
  .pulse-tabbar::before { display: none; }
}
