/* Peppy mobile theme.
 *
 * Ported from the "Peppy Mobile Theme" design document. That file specifies
 * every screen with inline styles; the repeated patterns are lifted into the
 * classes below so a change lands in one place rather than 25 screens. Values
 * are taken verbatim from the design - where a number looks arbitrary it is
 * because it is what the design says.
 *
 * Mobile-only by decision: the design defines a 390x844 frame and nothing for
 * desktop, so wide viewports get the same column, centred, rather than an
 * invented desktop layout.
 */

:root {
  /* Brand greens, darkest to lightest */
  --p-ink: #123f39;
  --p-ink-2: #153f38;
  --p-head: #0d5d52;
  --p-teal-deep: #068573;
  --p-teal: #0a9e8b;
  --p-teal-bright: #03a995;
  --p-teal-light: #3fbca7;

  /* Text */
  --p-body: #4f6f6a;
  --p-muted: #7c918d;
  --p-muted-2: #6d8882;
  --p-faint: #9db0ac;
  --p-eyebrow: #4f8b80;

  /* Surfaces */
  --p-canvas: #f4fbf9;
  --p-surface: #fff;
  --p-surface-tint: #f4fdfa;
  --p-line: #e8f2ef;
  --p-line-2: #dfeae7;
  --p-line-3: #e4f1ee;

  /* Accents */
  --p-amber-bg: #fffaf0;
  --p-amber-line: #f6e6cf;
  --p-amber-ink: #c78429;
  --p-amber-soft: #fff9ed;
  --p-amber-soft-ink: #9a7a41;
  --p-violet-bg: #f8f6ff;
  --p-violet-line: #e7e1f8;
  --p-violet-ink: #7161ba;
  --p-rose-bg: #fff0f3;
  --p-rose-ink: #e9829a;
  --p-rose-ink-deep: #c95f7c;

  /* Icon tiles: four rungs, glyph pinned at ~0.78 of the tile on each.
     Call sites used to hand-roll this geometry and had drifted badly - the same
     46px tile rendered its icon at 20px on one screen and 42px on another. The
     large ones are left over from when a tone's glyph was a text character;
     categoryIcon() emits an SVG now, and at 42px in a 46px tile the strokes
     spill past the rounded corners because it sets overflow:visible.

     0.78, up from the 0.48 it started at, because at 0.48 the icon read as a
     small mark floating in a large rounded square rather than as the tile's
     subject. The ceiling is the corner radius, and it was worth measuring: the
     art occupies 85% of its 24-unit viewBox (1.8 to 22.3), so a 36px glyph in a
     46px tile draws 30.8px, leaving 7.6px on every side and reaching 21.7px
     along the diagonal against a 25.5px limit. */
  --p-icon-lg: 46px;  --p-icon-lg-radius: 17px;  --p-icon-lg-glyph: 36px;
  --p-icon-md: 44px;  --p-icon-md-radius: 16px;  --p-icon-md-glyph: 34px;
  --p-icon-sm: 38px;  --p-icon-sm-radius: 14px;  --p-icon-sm-glyph: 30px;
  --p-icon-xs: 32px;  --p-icon-xs-radius: 12px;  --p-icon-xs-glyph: 25px;
  /* Every tile in the design is tilted; one value so they tilt together. */
  --p-icon-tilt: rotate(-4deg);

  /* Mint tints. Three roles, not six near-identical values. The old spread had
     #e2f8f2, #d9f2ec and #e6f7f3 all doing the same job as --p-mint-soft and
     #d7f2ea doing --p-mint's, which meant "selected" had no single colour a
     reader could learn. Note --p-mint is also FINDING_TONES.Established.bg:
     that one is semantic, decided in app.js, and must not be swapped for a
     decorative tint. */
  --p-mint-faint: #f2fcf8;  /* card wash */
  --p-mint-soft: #e6f4f1;   /* quiet fill: progress troughs, neutral pills */
  --p-mint: #dcf4ee;        /* icon tiles, active chips */

  /* Elevation. One rung per real depth. `soft` and `tile` differ by 2/255 of
     alpha and are indistinguishable, so `soft` is retired rather than kept as a
     rung nobody can pick correctly. */
  /* Deeper than they were, because the outline they used to sit beside is gone.
     A card is #fff on a #f4fbf9 page - 1.01:1, which is to say indistinguishable
     - so with no border the shadow is the only thing drawing the edge. At the
     old 5% it was decorative; at these values it is doing the work. Still soft:
     the design is airy and a hard shadow would read as a different app. */
  --p-shadow-card: 0 8px 22px #16866e26;
  --p-shadow-tile: 0 5px 16px #16866e1f;   /* cards, rows, tiles - the default */
  --p-shadow-control: 0 4px 12px #16866e12; /* back buttons, small controls */
  --p-shadow-rail: 0 4px 10px #16866e1f;    /* the calendar time rail */
  --p-shadow-cta: 0 8px 20px #037d6d33;     /* filled primary buttons */
  --p-shadow-nav: 0 8px 24px #16564d1f;

  --p-font: "Nunito", ui-rounded, system-ui, -apple-system, sans-serif;

  /* The width of the app column. 430px is the design's frame and stays the
     value on a phone; peppy-responsive.css steps it up past 650px. Read by
     .p-shell and by .p-drawer, which is position:fixed outside the shell and
     has to be told the same number to stay aligned with it. Dialogs are
     deliberately NOT on this variable - a dialog that grew with the shell would
     be a 900px-wide form on a desktop, which is unreadable. */
  --p-shell-w: 430px;
}

/* ---------------------------------------------------------------- shell -- */

body.peppy-theme {
  margin: 0;
  background: #eef3f3;
  font-family: var(--p-font);
  color: #26343c;
  -webkit-font-smoothing: antialiased;
}

/* The design's 390px frame. On anything wider the column simply centres -
   the design specifies no desktop layout, so none is invented here. */
.p-shell {
  position: relative;
  display: flex;
  flex-direction: column;
  width: 100%;
  max-width: var(--p-shell-w);
  /* Fixed viewport height, not min-height: the design's frame does not grow.
     The body scrolls inside it, which is what keeps the tab bar pinned to the
     bottom instead of drifting to the end of the content. */
  height: 100vh;
  height: 100dvh;
  /* The same element also carries the legacy .app-shell, which sets
     min-height:100vh. Used height is max(min-height, height), so without this
     the frame stays 100vh wherever that exceeds 100dvh - i.e. whenever mobile
     browser chrome is showing - and the absolutely positioned tab bar sits
     below the fold inside an overflow:hidden shell, unreachable. */
  min-height: 0;
  margin: 0 auto;
  background: var(--p-canvas);
  overflow: hidden;
}

/* The scroll chain has to survive every wrapper between shell and body, or the
   inner overflow never engages and the shell grows instead. */
body.peppy-theme .content {
  flex: 1 1 auto;
  min-height: 0;
  display: flex;
  flex-direction: column;
  overflow: hidden;
  /* The legacy `.content { margin: auto }` is a cross-axis auto margin inside
     this column flex shell, which cancels stretch and shrink-wraps each view to
     its widest child. Home only looked right because its content happens to
     fill the shell; More sat 25px in on both sides. Every view is the full
     shell width, and the design body owns the side padding. */
  width: 100%;
  margin-left: 0;
  margin-right: 0;
}
body.peppy-theme .app-view { display: none; }
body.peppy-theme .app-view.active {
  flex: 1 1 auto;
  min-height: 0;
  display: flex;
  flex-direction: column;
  overflow: hidden;
}

.p-brandbar {
  flex: none;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 16px 20px 10px;
  background: var(--p-canvas);
}
.p-brandbar img { display: block; width: 118px; height: 38px; object-fit: contain; }

.p-head { flex: none; padding: 14px 20px 6px; }
.p-head h1 {
  margin: 0 0 3px;
  font-size: 24px;
  font-weight: 900;
  letter-spacing: -.8px;
  color: var(--p-head);
}
.p-head p { margin: 0; color: var(--p-muted); font-size: 13px; font-weight: 700; }

/* 96px of bottom padding clears the floating tab bar. */
.p-body {
  flex: 1 1 auto;
  min-height: 0;
  overflow-y: auto;
  overflow-x: hidden;
  padding: 12px 20px 96px;
  display: flex;
  flex-direction: column;
  gap: 12px;
}

/* A list of cards or rows. One column here, which is every phone and the design
   as drawn; peppy-responsive.css turns it into a wrapping grid above 650px so a
   wide window buys more list on screen instead of more white space beside it.
   Two knobs, both set by the call site: --p-flow-gap for the rhythm between
   items, --p-flow-min for how narrow a column may get before the grid drops to
   fewer of them.
   The [hidden] guard is not optional. This rule sets `display`, so it outranks
   the UA stylesheet's `[hidden]{display:none}` - and #more-invite-group ships
   hidden and is revealed only for the administrator. Without the guard the
   invite row would be visible to every account. Specificity, not order, is what
   keeps it winning over the grid rule in the responsive layer. */
.p-flow { display: flex; flex-direction: column; gap: var(--p-flow-gap, 12px); }
.p-flow[hidden] { display: none; }

/* A group of rows on More, under one eyebrow heading. Tighter than a card list,
   and its rows are short enough to pack three across rather than two. */
.p-more-group { flex: none; --p-flow-gap: 8px; --p-flow-min: 260px; --p-flow-max: 400px; }

/* Home's two departures from the standard body. It opens on the greeting card,
   which carries its own top padding, so it starts tighter; and its children are
   whole cards rather than rows, so they want more air between them. */
body.peppy-theme [data-app-view="home"] > .p-body { padding-top: 4px; gap: 16px; }

/* Home, Calendar, More and Analysis each repeated .p-body and .p-head as an
   inline style rather than wearing the class. That made their side padding
   unreachable from a media query, because an inline declaration outranks every
   stylesheet rule. They wear the classes now; these two rules carry across the
   only values the inline copies actually differed on, so the phone layout is
   unchanged to the pixel. */
body.peppy-theme [data-app-view="more"] > .p-body { gap: 14px; }
body.peppy-theme [data-app-view="more"] > .p-head,
body.peppy-theme [data-app-view="analysis"] > .p-head { padding-top: 16px; }

.pscroll { overflow: auto; scrollbar-width: none; }
.pscroll::-webkit-scrollbar { display: none; }

@keyframes pbob { 0%, 100% { transform: translateY(0); } 50% { transform: translateY(-5px); } }

/* --------------------------------------------------------------- tabbar -- */

.p-tabbar {
  position: absolute;
  z-index: 30;
  left: 0; right: 0; bottom: 0;
  padding: 10px 12px 12px;
  pointer-events: none;
  /* The legacy .bottom-nav makes this a 5-column grid, which would collapse the
     floating bar into a single cell. It is only a positioning wrapper here. */
  display: block;
}
.p-tabbar-inner {
  display: grid;
  grid-template-columns: repeat(5, minmax(0, 1fr));
  align-items: center;
  height: 66px;
  padding: 6px;
  border-radius: 24px;
  background: var(--p-surface);
  box-shadow: var(--p-shadow-nav);
  pointer-events: auto;
}
.p-tab {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 4px;
  height: 54px;
  border: 0;
  border-radius: 18px;
  background: transparent;
  color: var(--p-muted);
  font: 900 10px var(--p-font);
  cursor: pointer;
}
.p-tab svg { display: block; width: 22px; height: 22px; }

/* Shown only where the tab bar is a sidebar - see peppy-responsive.css. Hidden
   here rather than in the desktop band so they stay hidden if that band is ever
   removed, instead of appearing above the floating bar on a phone. */
.p-sidebar-brand,
.p-sidebar-note { display: none; }
.p-tab.is-active { background: var(--p-mint-soft); color: var(--p-teal-deep); }

/* ---------------------------------------------------------------- cards -- */

.p-card {
  flex: none;
  position: relative;
  padding: 16px;
  border-radius: 24px;
  background: var(--p-surface);
  /* No outline, in either theme. The border is kept at its width and made
     transparent so nothing reflows; --p-shadow-card draws the edge instead. */
  border: 1px solid transparent;
  box-shadow: var(--p-shadow-card);
  display: flex;
  flex-direction: column;
  gap: 12px;
}

.p-row {
  display: grid;
  grid-template-columns: 44px minmax(0, 1fr) 14px;
  align-items: center;
  gap: 12px;
  padding: 12px 14px;
  border-radius: 20px;
  background: var(--p-surface);
  border: 1.5px solid var(--p-line-3);
  box-shadow: var(--p-shadow-tile);
  text-align: left;
  font: inherit;
  cursor: pointer;
  width: 100%;
}
.p-row strong { display: block; font-size: 15px; font-weight: 900; color: var(--p-ink-2); }
.p-row span { display: block; margin-top: 2px; color: var(--p-muted); font-size: 12px; font-weight: 700; }
.p-row b { color: #a3b8b3; font-size: 18px; font-weight: 400; }

/* Tilted icon tiles are a signature of the design - every one is rotated.
   Geometry only: colour stays inline at the call site, because it is
   tone-driven and scripts/build-dark-theme.mjs builds the dark palette by
   reading inline `background:#hex` declarations. Moving colour into a class
   would silently drop it from the dark theme. */
.p-icon-lg, .p-icon-md, .p-icon-sm, .p-icon-xs {
  display: grid;
  place-items: center;
  flex: none;
  font-weight: 900;
  transform: var(--p-icon-tilt);
}
.p-icon-lg { width: var(--p-icon-lg); height: var(--p-icon-lg); border-radius: var(--p-icon-lg-radius); font-size: var(--p-icon-lg-glyph); }
.p-icon-md { width: var(--p-icon-md); height: var(--p-icon-md); border-radius: var(--p-icon-md-radius); font-size: var(--p-icon-md-glyph); }
.p-icon-sm { width: var(--p-icon-sm); height: var(--p-icon-sm); border-radius: var(--p-icon-sm-radius); font-size: var(--p-icon-sm-glyph); }
.p-icon-xs { width: var(--p-icon-xs); height: var(--p-icon-xs); border-radius: var(--p-icon-xs-radius); font-size: var(--p-icon-xs-glyph); }
/* categoryIcon() sizes its svg at 1em so a text glyph and an SVG glyph track the
   same font-size, but a stylesheet rule beats that presentation attribute - so
   these are what actually decide the drawn size. */
.p-icon-lg svg { width: var(--p-icon-lg-glyph); height: var(--p-icon-lg-glyph); }
.p-icon-md svg { width: var(--p-icon-md-glyph); height: var(--p-icon-md-glyph); }
.p-icon-sm svg { width: var(--p-icon-sm-glyph); height: var(--p-icon-sm-glyph); }
.p-icon-xs svg { width: var(--p-icon-xs-glyph); height: var(--p-icon-xs-glyph); }

/* The section label above a card. There was no light-mode rule at all - only a
   `body.peppy-dark .p-eyebrow` that nothing could match - so every call site
   hand-rolled it and they drifted to four combinations. Tracking is .08em,
   which is what 44 of the 51 sites already used. */
/* The close chip in every dialog heading. This was deleted by mistake in the
   legacy-CSS purge: the dead-rule stripper matched `.icon-*` to catch the unused
   `.icon-molecule` / `.icon-brain` variants, and `.icon-button` matched too -
   while the class is still emitted 16 times in index.html. Without it the button
   falls back to the browser's native control. --p-mint-soft is the same #e6f4f1
   it always was. */
body.peppy-theme .dialog-heading .icon-button {
  display: grid;
  place-items: center;
  flex: none;
  width: 36px;
  height: 36px;
  border: 0;
  border-radius: 14px;
  background: var(--p-mint-soft);
  color: var(--p-teal-deep);
  font-size: 18px;
  font-weight: 900;
  cursor: pointer;
}

.p-eyebrow {
  flex: none;
  margin: 0;
  color: var(--p-eyebrow);
  font-size: 11px;
  font-weight: 900;
  letter-spacing: .08em;
}

.p-pill {
  align-self: flex-start;
  padding: 7px 12px;
  border-radius: 999px;
  font-size: 11px;
  font-weight: 900;
  white-space: nowrap;
}
.p-pill-link {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  padding: 8px 12px;
  border-radius: 999px;
  background: var(--p-mint-soft);
  color: var(--p-teal-deep);
  font-size: 12px;
  font-weight: 900;
  border: 0;
  cursor: pointer;
}

/* Tinted explainer panels: mint for what is known, amber for what is not. */
.p-note { padding: 12px; border-radius: 18px; }
.p-note span {
  display: flex;
  align-items: center;
  gap: 6px;
  margin-bottom: 4px;
  font-size: 11px;
  font-weight: 900;
  letter-spacing: .08em;
}
.p-note p { margin: 0; color: #58746e; font-size: 13px; line-height: 1.4; text-wrap: pretty; }
.p-note-known { background: var(--p-mint-faint); }
.p-note-known span { color: #248d75; }
.p-note-gap { background: var(--p-amber-bg); }
.p-note-gap span { color: #b37b26; }

.p-disclaimer {
  flex: none;
  margin: 0;
  padding: 12px 14px;
  border-radius: 18px;
  background: var(--p-amber-soft);
  color: var(--p-amber-soft-ink);
  font-size: 11px;
  line-height: 1.45;
}

.p-meta {
  display: flex;
  justify-content: space-between;
  gap: 8px;
  padding-top: 10px;
  border-top: 1.5px dashed #cbeade;
  color: #6d9289;
  font-size: 11px;
  font-weight: 800;
}

/* -------------------------------------------------------------- banners -- */

/* Mascot + one line of reassurance. Used at the top of most screens. */
.p-banner {
  margin-top: 5px;
  flex: none;
  position: relative;
  z-index: 3;
  display: grid;
  grid-template-columns: 94px minmax(0, 1fr);
  align-items: center;
  gap: 12px;
  padding: 12px 16px 12px 6px;
  border-radius: 22px;
  overflow: visible;
  background: linear-gradient(120deg, #eef7ff, #fff);
}
.p-banner img { width: 100px; height: auto; margin: -20px 0 -10px; object-fit: contain; }
.p-banner p { margin: 0; color: #5c7b8a; font-size: 12px; line-height: 1.45; text-wrap: pretty; }

/* ---------------------------------------------------------- stat tiles -- */

.p-stats { flex: none; display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 10px; }
.p-stat {
  position: relative;
  padding: 11px 12px;
  border-radius: 20px;
  box-shadow: var(--p-shadow-tile);
  overflow: hidden;
}
/* The badge deliberately overflows the corner; the tile clips it. */
.p-stat-badge {
  position: absolute;
  right: -15px;
  top: -15px;
  display: grid;
  place-items: center;
  width: 58px;
  height: 58px;
  border-radius: 50%;
  padding: 0 0 8px 8px;
}
.p-stat-badge svg { width: 20px; height: 20px; }
.p-stat strong {
  position: relative;
  display: block;
  margin-top: 14px;
  font-size: 26px;
  line-height: 1;
  letter-spacing: -1px;
}
.p-stat span { position: relative; display: block; margin-top: 3px; color: var(--p-muted); font-size: 11px; font-weight: 800; }
.p-stat-teal { background: var(--p-surface-tint); border: 1.5px solid #dff2ed; }
.p-stat-teal .p-stat-badge { background: var(--p-mint); color: var(--p-teal); }
.p-stat-teal strong { color: var(--p-teal); }
.p-stat-violet { background: var(--p-violet-bg); border: 1.5px solid var(--p-violet-line); }
.p-stat-violet .p-stat-badge { background: #e7e0fc; color: var(--p-violet-ink); }
.p-stat-violet strong { color: var(--p-violet-ink); }
.p-stat-amber { background: var(--p-amber-bg); border: 1.5px solid var(--p-amber-line); }
.p-stat-amber .p-stat-badge { background: #fbe8cd; color: var(--p-amber-ink); }
.p-stat-amber strong { color: var(--p-amber-ink); }

/* ------------------------------------------------------- schedule rows -- */

.p-section-head { display: flex; align-items: center; justify-content: space-between; gap: 12px; }
.p-section-head h2 { margin: 0; font-size: 19px; font-weight: 900; letter-spacing: -.6px; color: var(--p-ink); }

.p-plan { display: flex; flex-direction: column; gap: 10px; }
/* Time sits in a vertical rail down the left edge of each row. */
.p-plan-row { display: grid; grid-template-columns: 30px minmax(0, 1fr); align-items: stretch; }
.p-plan-time {
  display: grid;
  place-items: center;
  border-radius: 12px 0 0 12px;
  background: var(--p-teal-light);
  box-shadow: var(--p-shadow-rail);
}
.p-plan-time time {
  color: #fff;
  font-size: 12px;
  font-weight: 900;
  letter-spacing: .08em;
  writing-mode: vertical-rl;
  transform: rotate(180deg);
}
.p-plan-card {
  display: grid;
  grid-template-columns: 46px minmax(0, 1fr) 44px;
  align-items: center;
  gap: 11px;
  padding: 12px 13px;
  border-radius: 0 22px 22px 0;
  background: var(--p-surface-tint);
}
.p-plan-card strong { display: block; font-size: 16px; font-weight: 900; color: var(--p-ink-2); }
.p-plan-card > div > span { display: block; margin-top: 2px; color: var(--p-muted); font-size: 12px; font-weight: 800; }
.p-check {
  display: grid;
  place-items: center;
  width: 44px;
  height: 44px;
  border-radius: 16px;
  background: var(--p-teal);
  border: 0;
  color: #fff;
  font-size: 19px;
  box-shadow: 0 6px 14px #16866e33;
  cursor: pointer;
}
.p-check.is-done { background: var(--p-mint); color: var(--p-teal); box-shadow: none; }

/* --------------------------------------------------------------- input -- */

.p-search {
  flex: none;
  display: flex;
  align-items: center;
  gap: 9px;
  padding: 10px 14px;
  border-radius: 16px;
  /* Tinted rather than white: on a page of white cards a white search box read
     as another panel, giving no hint it could be typed into. The tint alone
     carries that - no border, and kept shallow so it stays a search field
     rather than another panel competing with the cards. */
  background: #f4f7f6;
  border: 0;
  box-shadow: none;
  color: #adbbb7;
  font-size: 14px;
  font-weight: 800;
}
.p-search:focus-within { background: #edf2f1; }
/* The glyph carries the "this is a search" cue on its own now that there is no
   border, so it is a step larger than the label text beside it. */
.p-search > span {
  flex: none;
  font-size: 18px;
  line-height: 1;
}
.p-search input::placeholder { color: #adbbb7; opacity: 1; }
.p-search input {
  width: 100%;
  border: 0;
  outline: 0;
  /* The wrapper supplies the padding; the input's own would only make the
     field tall. (mobile-fixes.css used to force 11px here with !important -
     that half of the rule is gone, so plain padding is enough now.) */
  padding: 0;
  background: transparent;
  font: 800 14px var(--p-font);
  color: var(--p-ink-2);
}

.p-chips { flex: none; display: flex; gap: 8px; overflow: auto; }
.p-chip {
  flex: none;
  padding: 9px 14px;
  border-radius: 999px;
  background: var(--p-surface);
  border: 1.5px solid var(--p-line-2);
  color: var(--p-body);
  font-size: 12px;
  font-weight: 900;
  cursor: pointer;
}
.p-chip.is-active { background: var(--p-mint-soft); border-color: #a8e0d3; color: var(--p-teal-deep); }

.p-fav {
  display: grid;
  place-items: center;
  width: 38px;
  height: 38px;
  flex: none;
  border: 0;
  border-radius: 14px;
  background: var(--p-rose-bg);
  color: var(--p-rose-ink);
  font-size: 16px;
  transform: rotate(4deg);
  cursor: pointer;
}

/* ------------------------------------------------------------- calendar -- */

.p-cal-nav { display: flex; align-items: center; gap: 6px; }
.p-cal-arrow {
  display: grid;
  place-items: center;
  width: 32px;
  height: 32px;
  border: 1.5px solid var(--p-line-2);
  border-radius: 12px;
  background: var(--p-surface);
  color: var(--p-teal-deep);
  font: 900 16px var(--p-font);
  cursor: pointer;
}

/* The calendar grid keeps its own internal layout (month-days, week-strip);
   the theme only supplies the surrounding chrome. */

/* ------------------------------------------------------------- listings -- */

/* Shared shape for the vial, history, protocol and curation lists so every
   rendered row picks up the theme without each renderer repeating itself. */
.vial-inventory-list, .history-list, .protocol-list, .curation-list, .analysis-list, .analysis-stack-list {
  display: flex;
  flex-direction: column;
  gap: 10px;
}
.vial-inventory-list > *, .history-list > *, .protocol-list > *, .curation-list > *, .analysis-list > * {
  padding: 14px 16px;
  border-radius: 20px;
  background: var(--p-surface);
  border: 1.5px solid var(--p-line-3);
  box-shadow: var(--p-shadow-tile);
}
.analysis-stack-list { flex-direction: row; flex-wrap: wrap; gap: 8px; }
.analysis-stack-list > span {
  padding: 9px 14px;
  border-radius: 999px;
  background: var(--p-mint-soft);
  color: var(--p-teal-deep);
  font-size: 12px;
  font-weight: 900;
}

/* Empty-state copy used by several renderers. */
.empty-curation, .empty-state {
  margin: 0;
  padding: 14px 16px;
  border-radius: 20px;
  background: var(--p-surface);
  border: 1.5px dashed var(--p-line-2);
  color: var(--p-muted);
  font-size: 13px;
  font-weight: 700;
  text-align: center;
}

/* --------------------------------------------------------------- dialogs -- */

/* Every dialog shares <dialog><form><div class="dialog-heading"> plus <label>
   wrapped fields, so the whole set themes from here without touching markup. */
body.peppy-theme dialog {
  width: min(430px, calc(100vw - 24px));
  max-height: 88vh;
  padding: 0;
  border: 0;
  border-radius: 28px;
  background: var(--p-canvas);
  color: var(--p-body);
  box-shadow: 0 26px 60px #14343033;
  overflow: auto;
  scrollbar-width: none;
}
body.peppy-theme dialog::-webkit-scrollbar { display: none; }
body.peppy-theme dialog::backdrop { background: #0d3d3699; backdrop-filter: blur(2px); }
body.peppy-theme dialog > form,
body.peppy-theme dialog > section {
  display: flex;
  flex-direction: column;
  gap: 12px;
  padding: 20px;
}

body.peppy-theme .dialog-heading {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 12px;
  margin-bottom: 2px;
}
body.peppy-theme .dialog-heading .eyebrow {
  margin: 0 0 3px;
  color: var(--p-eyebrow);
  font-size: 11px;
  font-weight: 900;
  letter-spacing: .08em;
}
body.peppy-theme .dialog-heading h2,
body.peppy-theme .dialog-heading h3 {
  margin: 0;
  font-size: 22px;
  font-weight: 900;
  letter-spacing: -.7px;
  color: var(--p-head);
}

/* Fields: the label text sits above a rounded control. Both surfaces that
   carry a form - the centred dialog and the bottom sheet - share these, so the
   record editor reads the same whichever one it is shown in. */
body.peppy-theme dialog label,
body.peppy-theme .p-drawer label {
  display: flex;
  flex-direction: column;
  gap: 6px;
  color: var(--p-ink-2);
  font-size: 12px;
  font-weight: 900;
  letter-spacing: .01em;
}
body.peppy-theme dialog label small,
body.peppy-theme .p-drawer label small {
  color: var(--p-muted);
  font-size: 11px;
  font-weight: 700;
  letter-spacing: 0;
}
body.peppy-theme dialog input,
body.peppy-theme dialog select,
body.peppy-theme dialog textarea,
body.peppy-theme .p-drawer input,
body.peppy-theme .p-drawer select,
body.peppy-theme .p-drawer textarea {
  width: 100%;
  padding: 12px 14px;
  border: 1.5px solid var(--p-line-2);
  border-radius: 16px;
  background: var(--p-surface);
  color: var(--p-ink-2);
  font: 800 14px var(--p-font);
}
body.peppy-theme dialog input:focus,
body.peppy-theme dialog select:focus,
body.peppy-theme dialog textarea:focus,
body.peppy-theme .p-drawer input:focus,
body.peppy-theme .p-drawer select:focus,
body.peppy-theme .p-drawer textarea:focus {
  outline: 0;
  border-color: #8fd8c9;
  box-shadow: 0 0 0 3px #0a9e8b1f;
}
body.peppy-theme dialog textarea,
body.peppy-theme .p-drawer textarea { resize: vertical; min-height: 68px; }
body.peppy-theme .form-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; }

body.peppy-theme dialog .primary-button,
body.peppy-theme .p-drawer .primary-button {
  padding: 14px 18px;
  border: 0;
  border-radius: 18px;
  background: var(--p-teal);
  color: #fff;
  font: 900 15px var(--p-font);
  box-shadow: 0 8px 18px #16866e33;
  cursor: pointer;
}
/* Spent: saved, and nothing changed since. Still has to be readable - "Saved!"
   is the confirmation, not decoration - so this clears 4.5:1 rather than
   sitting at the grey a disabled control is usually allowed. */
body.peppy-theme dialog .primary-button:disabled,
body.peppy-theme .p-drawer .primary-button:disabled {
  background: #d5e5e1;
  color: #4a655f;
  box-shadow: none;
  cursor: default;
}
/* Irreversible actions only - deleting the account. Not a second accent to
   reach for: the point is that it looks unlike every other button in the app.

   #c62839 rather than the #e9566d used on Privacy's erase button, which is a
   panel-tinted control rather than a standalone one. White on #e9566d is 2.9:1;
   this clears 5.58:1, and 15px/900 is below the 18.66px bold that would let
   large-text 3:1 apply. */
body.peppy-theme dialog .primary-button.danger-button,
body.peppy-theme .p-drawer .primary-button.danger-button {
  background: #c62839;
  box-shadow: 0 8px 18px #c6283933;
}
body.peppy-theme dialog .primary-button.danger-button:disabled,
body.peppy-theme .p-drawer .primary-button.danger-button:disabled {
  background: #e3d4d6;
  color: #6a4a4f;
  box-shadow: none;
}
body.peppy-theme dialog .secondary-button,
body.peppy-theme .p-drawer .secondary-button {
  padding: 13px 18px;
  border: 1.5px solid var(--p-line-2);
  border-radius: 18px;
  background: var(--p-surface);
  color: var(--p-teal-deep);
  font: 900 14px var(--p-font);
  cursor: pointer;
}
body.peppy-theme dialog .text-button,
body.peppy-theme .p-drawer .text-button {
  border: 0;
  background: transparent;
  color: var(--p-teal-deep);
  font: 900 13px var(--p-font);
  cursor: pointer;
}
body.peppy-theme dialog .form-disclaimer,
body.peppy-theme .p-drawer .form-disclaimer {
  margin: 0;
  padding: 12px 14px;
  border-radius: 18px;
  background: var(--p-amber-soft);
  color: var(--p-amber-soft-ink);
  font-size: 11px;
  font-weight: 700;
  line-height: 1.45;
}

/* --------------------------------------------------------------- landing -- */

/* The one place a person meets Peppy before it is theirs, so it is built from
   the same parts as the rest of it: the greeting card's gradient and corner
   circle, the shared icon-tile geometry, the shared type ramp.
   
   This block is now the only definition of the landing screen. There used to be
   a second, older one in mobile-fixes.css; the two disagreed about alignment,
   type sizes and whether the tiles had borders, and because this file wins on
   specificity the other one's rules only took effect where this one happened to
   be silent. Editing either produced a screen neither described.
   
   It also has no dark variant on purpose: it is shown before an account exists,
   so before any theme preference has been read. */
body.peppy-theme .landing-screen {
  position: fixed;
  inset: 0;
  z-index: 40;
  display: grid;
  /* `safe` is the whole point of this line, not decoration. Plain `center` on a
     grid overflows the item equally top and bottom when the content is taller
     than the screen, and the top overflow cannot be scrolled to - the wordmark
     would simply be cut off and unreachable on a short phone, or with large
     text sizes turned on. `safe` falls back to start-alignment in exactly that
     case, so the screen centres when it fits and scrolls from the top when it
     does not. */
  place-items: safe center;
  padding: 0;
  background: linear-gradient(160deg, #d9f6ee, #f4fbf9 60%);
  overflow: auto;
}
/* Full-bleed: a phone screen, not a card sitting on a desktop page. The
   horizontal padding holds the content to a readable column and centres it on
   anything wider, so the background fills the screen without the text
   stretching with it. */
body.peppy-theme .landing-card {
  width: 100%;
  display: flex;
  flex-direction: column;
  align-items: stretch;
  gap: 14px;
  padding: 26px max(20px, calc((100% - 400px) / 2));
  border: 0;
  border-radius: 0;
  background: none;
  box-shadow: none;
  text-align: left;
}
body.peppy-theme .landing-logo { width: 132px; height: auto; margin: 0 auto 2px; object-fit: contain; }

/* Deliberately Home's greeting card: same gradient, same 26px radius, same
   corner circle, mascot on the same side. The screen before signing in and the
   first screen after it should read as one product rather than two. */
body.peppy-theme .landing-hero {
  position: relative;
  padding: 22px 20px;
  border-radius: 26px;
  overflow: hidden;
  text-align: center;
  background: linear-gradient(140deg, #d9f6ee, #fff8e4 118%);
}
/* The card clips its own corner circle now that nothing needs to cross the
   edge. The wrapper stays because it keeps the circle behind the text without
   a second stacking context on the copy. */
body.peppy-theme .landing-hero-clip { position: absolute; inset: 0; border-radius: 26px; overflow: hidden; pointer-events: none; }
body.peppy-theme .landing-hero-clip::before {
  content: "";
  position: absolute;
  right: -46px;
  top: -54px;
  width: 152px;
  height: 152px;
  border: 22px solid #ffffff8c;
  border-radius: 50%;
}
body.peppy-theme .landing-hero-copy { position: relative; z-index: 1; min-width: 0; }
body.peppy-theme .landing-eyebrow { margin: 0 0 4px; color: #3e6d64; font-size: 11px; font-weight: 900; letter-spacing: .08em; }
body.peppy-theme .landing-card h1 {
  margin: 0 0 6px;
  color: var(--p-head);
  font-size: 23px;
  font-weight: 900;
  letter-spacing: -.6px;
  line-height: 1.16;
  text-wrap: pretty;
}
body.peppy-theme .landing-sub { margin: 0 auto; max-width: 30ch; color: #4c6b65; font-size: 12.5px; line-height: 1.45; }
/* Four tiles, icon centred over the label: laying the icon beside the label
   left every tile ragged, because "Track" is half the width of "Understand".
   Stacked, all four are structurally identical whatever the word is.
   
   Borderless with the shared card lift, because the outline came off cards
   across both themes and these were the last ones still wearing one. Tiles use
   the shared --p-icon-xs geometry and the shared tilt, so they cannot drift
   from the tiles everywhere else the way hand-rolled ones did. */
/* Four across. Two up made each card roughly 163px wide around a 44px tile -
   nearly four times the width of the thing it was framing - so the cards read
   as large empty rectangles with a small mark floating in them. Four columns
   brings the card to about 91px, close to square, and the tile fills half of
   it. The icon size is unchanged; only the frame around it moved. */
body.peppy-theme .landing-pillars { display: grid; grid-template-columns: repeat(4, 1fr); gap: 8px; width: 100%; }
/* Two zones per card: a coloured field carrying the icon, and a white strip
   under it carrying the word.
   
   Still one hue in three tones per card, but the tones moved. The fields used
   to be the hue at about half strength - pretty, and so pale that the glyph,
   which was drawn in the card's own colour, measured 1.6:1 against the tile it
   sat on. That is under the 3:1 a graphical object needs, so on four cards the
   icon was decorative rather than legible. The field is now the hue at close to
   full strength, the tile is a paler tint of it, and the glyph is a deeper step
   rather than a repeat of the field: 4.1 to 5.5:1, and a good deal livelier.
   What changes structurally is that the label leaves the field entirely. It was the one element the tonal scheme could not serve: text has
   to be read, and a readable word needed a dark step that belonged to none of
   the three tones around it. On white it can simply be the accent at full
   strength, which ties it back to the card rather than fighting it.
   
   The card clips its own corners, so the white strip needs no radius of its
   own and cannot drift from the card's. */
body.peppy-theme .landing-pillars span {
  display: grid;
  grid-template-rows: 1fr auto;
  justify-items: stretch;
  gap: 0;
  padding: 0;
  border: 0;
  /* Tighter at the bottom than the top. The white strip takes its corners from
     this radius, because the card clips it - giving the strip its own smaller
     radius instead would round it inside the card's wider arc and leave a
     sliver of the colour showing in each bottom corner. */
  border-radius: 18px 18px 11px 11px;
  overflow: hidden;
  background: #17b39c;
  font-size: 11px;
  font-weight: 900;
  letter-spacing: -.1px;
  text-align: center;
  box-shadow: 0 6px 16px #16866e1f;
}
body.peppy-theme .landing-pillars b {
  justify-self: center;
  align-self: center;
  display: grid;
  place-items: center;
  width: var(--p-icon-md);
  height: var(--p-icon-md);
  margin: 13px 0 12px;
  border-radius: var(--p-icon-md-radius);
  background: #cdf1e9;
  color: #0e7f6d;
  transform: var(--p-icon-tilt);
  box-shadow: none;
}
/* rotate(4deg) cancels the tile's tilt so the glyph sits upright in a tilted
   tile. The nudges below are separate: place-items:center centres the SVG's
   box, not the ink inside it, and two of these four are drawn bottom-heavy -
   the bar chart stands on a baseline at y=20.8 and the book's spine hangs
   below its centre - so their marks sat low in the tile while the box was
   perfectly centred. Measured as the ink bounding box, padded by half the
   stroke, against the 24-unit viewBox centre: Understand was 1.2 units low and
   Analyse 2.7.
   
   Expressed as a percentage of the glyph box rather than in px so the
   correction survives a change to --p-icon-md-glyph, and translated before
   rotating so the nudge runs true vertically rather than along the tilt. */
body.peppy-theme .landing-pillars b svg { width: var(--p-icon-md-glyph); height: var(--p-icon-md-glyph); transform: rotate(4deg); }
body.peppy-theme .landing-pillars span:nth-child(2) b svg { transform: translateY(-5%) rotate(4deg); }
body.peppy-theme .landing-pillars span:nth-child(3) b svg { transform: translateY(-11.25%) rotate(4deg); }

/* One ink for all four labels, not the accent per card. --p-ink-2 is what every
   other card title in the app is written in, so the words read as a row of
   labels rather than as four differently coloured things - the colour is
   already carried by the field above each one. It measures 11.0 on white. */
body.peppy-theme .landing-pillars i {
  display: block;
  padding: 8px 3px;
  background: #fff;
  color: var(--p-ink-2);
  font-style: normal;
  line-height: 1.2;
}
body.peppy-theme .landing-pillars span:nth-child(2) { background: #8a76dd; box-shadow: 0 6px 16px #7161ba2e; }
body.peppy-theme .landing-pillars span:nth-child(2) b { background: #e3ddfb; color: #5a45ad; }
body.peppy-theme .landing-pillars span:nth-child(3) { background: #e2a02f; box-shadow: 0 6px 16px #a06a152e; }
body.peppy-theme .landing-pillars span:nth-child(3) b { background: #fbe8c6; color: #96650f; }
/* Rose rather than a second teal - Track and Calculate were a shade apart and
   read as the same accent. */
body.peppy-theme .landing-pillars span:nth-child(4) { background: #eb7b9d; box-shadow: 0 6px 16px #b8506e2e; }
body.peppy-theme .landing-pillars span:nth-child(4) b { background: #fddce6; color: #b53f63; }

body.peppy-theme .landing-actions { display: flex; flex-direction: column; gap: 9px; width: 100%; }
body.peppy-theme .landing-actions .primary-button {
  min-height: 52px;
  padding: 0 18px;
  border: 0;
  border-radius: 18px;
  background: var(--p-teal-deep);
  color: #fff;
  font: 900 15px var(--p-font);
  box-shadow: 0 8px 18px #16866e33;
  cursor: pointer;
}
body.peppy-theme .landing-actions .secondary-button {
  min-height: 50px;
  padding: 0 18px;
  border: 1.5px solid #b9e6da;
  border-radius: 18px;
  background: #ffffffcc;
  color: var(--p-teal-deep);
  font: 900 14px var(--p-font);
  cursor: pointer;
}
/* Darker than --p-muted, which measures 3.06 on this background. A disclaimer
   nobody can read is not a disclaimer. */
body.peppy-theme .landing-note { margin: 0; color: #5f736f; font-size: 11px; line-height: 1.45; text-align: center; }

/* These must out-specify the .landing-screen rule above, or `display: grid`
   beats the hidden attribute and the landing overlay never goes away after
   sign-in. Same for the auth-pending splash. */
body.peppy-theme .landing-screen[hidden] { display: none; }
body.peppy-theme.auth-pending .landing-screen { display: none; }

/* -------------------------------------------------------- compound record -- */

/* Design 8a-8d: one card with four tabs. Themed via CSS so the existing tab
   switching in app.js keeps working untouched. */
body.peppy-theme .compound-tabs {
  display: flex;
  gap: 8px;
  overflow: auto;
  scrollbar-width: none;
  padding-bottom: 2px;
}
body.peppy-theme .compound-tabs::-webkit-scrollbar { display: none; }
body.peppy-theme .compound-tab {
  flex: none;
  padding: 9px 14px;
  border: 1.5px solid var(--p-line-2);
  border-radius: 999px;
  background: var(--p-surface);
  color: var(--p-body);
  font: 900 12px var(--p-font);
  cursor: pointer;
}
body.peppy-theme .compound-tab.active { background: var(--p-mint-soft); border-color: #a8e0d3; color: var(--p-teal-deep); }

body.peppy-theme .compound-view { display: none; flex-direction: column; gap: 12px; }
body.peppy-theme .compound-view.active { display: flex; }

/* Snapshot strip: evidence label, source count, review date. */
body.peppy-theme .evidence-snapshot {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(0, 1fr));
  gap: 8px;
  margin-top: 12px;
}

body.peppy-theme .analysis-stack-list > span {
  padding: 9px 14px;
  border-radius: 999px;
  background: var(--p-mint-soft);
  color: var(--p-teal-deep);
  font-size: 12px;
  font-weight: 900;
}

/* ------------------------------------------------- legacy inset removal -- */

/* The old .content wrapper adds its own 16px side padding and an 89px bottom
   band. Stacked on the design's own 20px it read as a very thick inset, and
   doubled the tab-bar clearance. The design body owns spacing now. */
body.peppy-theme .content { padding: 0 !important; }

/* Mobile app: no scrollbar furniture anywhere inside the frame. */
body.peppy-theme .p-shell *,
body.peppy-theme dialog * { scrollbar-width: none; -ms-overflow-style: none; }
body.peppy-theme .p-shell *::-webkit-scrollbar,
body.peppy-theme dialog *::-webkit-scrollbar { width: 0; height: 0; display: none; }

/* The floating bar is the white pill itself; its wrapper must never paint. */
body.peppy-theme .p-tabbar { background: transparent !important; border: 0; box-shadow: none; }

/* -------------------------------------------------------------- subpages -- */

/* Design 7a-7i: full-screen pages with their own header and no tab bar. */
.p-subpage {
  position: absolute;
  inset: 0;
  /* Below the tab bar (z-index 30) so it stays reachable. The design shows
     these pages without it, but navigation is kept available by choice. */
  z-index: 25;
  display: flex;
  flex-direction: column;
  background: var(--p-canvas);
  overflow: hidden;
}
.p-subpage[hidden] { display: none; }

/* Design 9b: status is a segmented control, not a dropdown. */
body.peppy-theme .p-status-field { display: flex; flex-direction: column; gap: 6px; }
body.peppy-theme .p-field-label { color: var(--p-ink-2); font-size: 12px; font-weight: 900; }
body.peppy-theme .p-segmented { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 8px; }
body.peppy-theme .p-segment {
  padding: 12px 10px;
  border: 1.5px solid var(--p-line-2);
  border-radius: 16px;
  background: var(--p-surface);
  color: var(--p-body);
  font: 900 13px var(--p-font);
  cursor: pointer;
}
body.peppy-theme .p-segment.is-active {
  background: var(--p-teal);
  border-color: var(--p-teal);
  color: #fff;
  box-shadow: 0 6px 14px #16866e33;
}

/* Sub-pages slide in from the right and can be swiped away in the same
   direction. During a drag the transform follows the finger with no
   transition; `is-settling` puts it back for the release animation. */
/* will-change only while a transform is actually running. Left on permanently
   it pins a full-viewport compositing layer for a page that is usually closed,
   which the spec warns against and which some WebViews handle badly. */
.p-subpage { touch-action: pan-y; }
.p-subpage.is-entering, .p-subpage.is-settling { will-change: transform; }
.p-subpage.is-entering { animation: p-subpage-in .26s cubic-bezier(.22, .61, .36, 1) both; }
.p-subpage.is-settling { transition: transform .26s cubic-bezier(.22, .61, .36, 1); }
/* A shadow along the leading edge sells the page as sitting above More.
   This was a ::before at left:-14px, which .p-subpage's own overflow:hidden
   clipped away - it has never been visible. An element's box-shadow is not
   clipped by that element's overflow, so it is one here instead.
   At rest the page fills the shell and the shadow falls outside it, where the
   shell clips it and there is nothing to see anyway. It earns its keep while
   the page is moving: sliding in, and under a finger dragging it back, the
   leading edge is mid-screen and the shadow lands on the page underneath. */
.p-subpage { box-shadow: -12px 0 22px #12352f24; }
/* The mascot's bob, compressed into one beat and given a wobble: a day you have
   just tapped overshoots, tips one way, tips back, and settles. Transform only,
   so nothing reflows and the grid does not move around it. z-index lifts the
   card over its neighbours for the moment it is bigger than they are.
   It is handed out on a pick, not on every render, so logging a dose does not
   replay it - see pickedDate in app.js. `body.peppy-no-motion *` kills all
   animation, so the Peppy animations switch turns this off with everything
   else. */
/* The numbers are handed in per tap as custom properties, so no two picks play
   the same way - see rollWobble() in app.js. The fallbacks are the original
   fixed values, which is what plays if anything ever renders a picked card
   without rolling for it. */
@keyframes p-day-pick {
  0%   { transform: scale(1) rotate(0deg); }
  30%  { transform: scale(var(--pick-s, 1.13))  rotate(var(--pick-r, -4deg)); }
  55%  { transform: scale(var(--pick-s2, .97))  rotate(var(--pick-r2, 3deg)); }
  78%  { transform: scale(var(--pick-s3, 1.04)) rotate(var(--pick-r3, -1.4deg)); }
  100% { transform: scale(1) rotate(0deg); }
}
/* The record's tab pill lands the same way a calendar day does, off the same
   keyframes and the same roll - see PeppyWobble in app.js. Two effects that
   merely looked alike would drift; one that is literally the same cannot.
   position/z-index for the same reason as the days: it is briefly larger than
   its neighbours and has to sit over them rather than push them. */
body.peppy-theme .week-day.is-picked,
body.peppy-theme .month-day.is-picked,
body.peppy-theme .compound-filter.is-picked,
body.peppy-theme [data-compound-tab].is-tab-picked {
  position: relative;
  z-index: 1;
  animation: p-day-pick var(--pick-d, .44s) cubic-bezier(.34, 1.4, .64, 1) both;
}
/* Room for that wobble. The filter row is overflow:auto on both axes, so a chip
   that scales or tips gets its top and bottom clipped off. The padding buys
   headroom inside the scroll box and the negative margin hands the space back
   to the layout, so nothing moves. */
body.peppy-theme #compound-filters { padding: 7px 0; margin: -7px 0; }
@keyframes p-subpage-in { from { transform: translateX(100%); } to { transform: none; } }

@media (prefers-reduced-motion: reduce) {
  .p-subpage.is-entering { animation: none; }
  .p-subpage.is-settling { transition: none; }
}

/* The sub-page's scroll container must also yield the horizontal axis, or the
   browser claims the gesture natively and cancels the swipe before it starts.
   This is why the swipe worked under synthetic events but not under a finger. */
.p-subpage .pscroll, .p-subpage [style*="overflow:auto"] { touch-action: pan-y; }

/* ------------------------------------------------------ tap behaviour -- */

/* No blue flash or text selection on chrome that behaves like a control.
   Content text stays selectable. */
body.peppy-theme {
  -webkit-tap-highlight-color: transparent;
}
body.peppy-theme a,
body.peppy-theme button,
body.peppy-theme .p-tab,
body.peppy-theme .p-row,
body.peppy-theme .p-chip,
body.peppy-theme .p-segment,
body.peppy-theme .week-day,
body.peppy-theme .month-day,
body.peppy-theme [data-more-action],
body.peppy-theme [data-calendar-mode],
body.peppy-theme [data-subpage-back] {
  -webkit-tap-highlight-color: transparent;
  -webkit-touch-callout: none;
  user-select: none;
  -webkit-user-select: none;
}
/* ------------------------------------------------------------------ holo --
   Premium's marks: the crown, and the icon on each of the five feature cards.
   Nothing outside Premium wears this.
   Two gradient layers over one another: an iridescent gold sheet, and a
   narrower white streak that reads as the light catching it. Both are sized
   larger than the tile and slid around by --holo-x / --holo-y, which
   app.js drives from the device's tilt. The two sizes differ so they travel at
   different apparent speeds - moving together reads as a texture sliding past,
   moving apart reads as a surface catching light.
   Every stop is a light one. The glyph sits on top at a fixed dark gold, and a
   gradient that swung dark anywhere would take the icon with it - see the
   contrast note in app.js. */
body.peppy-theme .p-holo {
  background:
    linear-gradient(115deg, #ffffff00 41%, #fffdf2b8 50%, #ffffff00 59%),
    linear-gradient(115deg, #a9780c 0%, #d7a63f 12%, #f7e6a8 25%, #fffdf0 36%, #f2da8d 48%, #cf9c28 60%, #a87709 72%, #e3c165 86%, #a9780c 100%);
  background-size: 240% 240%, 330% 330%;
  background-position: var(--holo-x, 50%) var(--holo-y, 50%), var(--holo-x, 50%) var(--holo-y, 50%);
  /* The bar's own edges: lit along the top, in shadow along the bottom, with a
     dark rim holding it together. Insets, so they stay put while the metal
     slides underneath them - an edge that travelled with the gradient would
     read as a picture of a bar rather than as a bar. */
  box-shadow:
    inset 0 1.5px 0 #fff5d1a6,
    inset 0 -1.5px 0 #4e370352,
    inset 0 0 0 1px #8a640f7a;
  /* A deep groove, not a gold-on-gold line. Truer to engraving - a channel cut
     into metal reads dark because it is in shadow - and it is what keeps the
     crown legible when the darkest band of the ramp slides under it. At the
     shallower gold this sat at 2:1 there; at this depth the worst point on the
     whole ramp is 3.47:1. */
  color: #3d2a01;
}
/* Carved in, not drawn on. The glyph is a dark groove with a lit lower edge and
   a shaded upper one, which is what the eye reads as depth. The offsets are
   sub-pixel on purpose: at 22px anything larger stops looking like an imprint
   and starts looking like a drop shadow. */
body.peppy-theme .p-holo svg {
  filter:
    drop-shadow(.6px 1px .4px #fff8dbde)
    drop-shadow(-.4px -.6px .4px #4a340299);
}
/* The card version of the gold bar: the same two layers driven by the same two
   properties, at a fraction of the strength. A specular streak and a very faint
   iridescent wash over whatever tint the card already has, so it reads as a
   sheen on the surface rather than as a second colour.

   ::before at z-index 0 with the children lifted to 1, not ::after: an overlay
   painted after the content sits on top of it, and a white veil at any alpha
   over body text is a legibility problem rather than an effect. */
body.peppy-theme .p-holo-card { position: relative; overflow: hidden; }
body.peppy-theme .p-holo-card > * { position: relative; z-index: 1; }
body.peppy-theme .p-holo-card::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 0;
  pointer-events: none;
  background:
    linear-gradient(115deg, #ffffff00 41%, #ffffff59 50%, #ffffff00 59%),
    linear-gradient(115deg, #c9a8ff00 0%, #c9a8ff3d 20%, #a8d8ff3d 40%, #a8f0d83d 58%, #ffc9ec3d 78%, #c9a8ff00 100%);
  background-size: 260% 260%, 340% 340%;
  background-position: var(--holo-x, 50%) var(--holo-y, 50%), var(--holo-x, 50%) var(--holo-y, 50%);
}
@media (prefers-reduced-motion: reduce) {
  body.peppy-theme .p-holo-card::before { background-position: 50% 50%, 50% 50%; }
}

/* The mascot banners. They already carry a soft two-stop gradient; this makes
   that gradient *travel* with the tilt rather than sit still. Same colours and
   the same look at rest - the light just moves across it.

   Deliberately gentler than the gold: a banner is background, and one that
   shimmered as hard as Premium's crown would outshout the thing it sits under.

   The doubled class and the !important are both about the cascade, not taste.
   The design writes `background:` inline as a shorthand, which resets size and
   position to their initial values, and the generated dark rules rewrite that
   same shorthand later in the file and also with !important. A doubled class
   outranks both.

   No overflow:hidden, because .p-banner is overflow:visible on purpose - the
   mascot hangs out of the card on negative margins and clipping would behead
   it. The ::before takes `border-radius: inherit` instead, which rounds its own
   corners without touching anything else. */
body.peppy-theme .p-holo-banner.p-holo-banner {
  position: relative;
  background-size: 220% 220% !important;
  background-position: var(--holo-x, 50%) var(--holo-y, 50%) !important;
}
body.peppy-theme .p-holo-banner.p-holo-banner > * { position: relative; z-index: 1; }
body.peppy-theme .p-holo-banner.p-holo-banner::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 0;
  pointer-events: none;
  border-radius: inherit;
  /* One iridescent wash at low alpha, travelling at a different rate from the
     gradient underneath. Two layers moving at one speed read as a texture
     sliding past; at different speeds they read as a surface catching light. */
  background: linear-gradient(115deg, #c9a8ff00 0%, #c9a8ff2b 22%, #a8d8ff2b 42%, #a8f0d82b 60%, #ffc9ec2b 80%, #c9a8ff00 100%);
  background-size: 320% 320%;
  background-position: var(--holo-x, 50%) var(--holo-y, 50%);
}
@media (prefers-reduced-motion: reduce) {
  body.peppy-theme .p-holo-banner.p-holo-banner,
  body.peppy-theme .p-holo-banner.p-holo-banner::before { background-position: 50% 50% !important; }
}

/* The green sticker on a logged dose, on Home and the Calendar.

   The same two layers as the cards and the faintest of the three: a 38px disc
   catching as much light as a full-width banner would read as a bug rather than
   as a finish. The white streak is 12% and the iridescent wash 13%, against the
   card's 35% and 24%.

   Sizes are the card's, not the banner's, so the sheen crosses a 38px circle at
   a speed you can see - a 320% sheet over something this small barely moves
   through it before the phone is level again.

   THE BAND IS WIDER THAN THE DISC, ON PURPOSE

   The streak runs 26% to 74% of its sheet rather than 40% to 60%. At a 260%
   sheet that is 1.25 disc-widths of lit band against the 0.52 it was, and the
   difference is what the sticker is made of: a narrow bright line reads as a
   glint off something curved, and this is a flat piece of vinyl. Wider than the
   disc means the whole face lifts and settles as the phone turns, instead of a
   highlight sliding across it.

   The peak is untouched at 12%, so nothing about the contrast below changed -
   only how much of the disc is at it.

   overflow:hidden is what clips the wash to the circle. It does not touch the
   white ring: that is a spread shadow, and a shadow is painted outside the box
   this clips. */
body.peppy-theme .p-holo-dose { position: relative; overflow: hidden; }
body.peppy-theme .p-holo-dose > * { position: relative; z-index: 1; }
body.peppy-theme .p-holo-dose::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 0;
  pointer-events: none;
  border-radius: inherit;
  background:
    linear-gradient(115deg, #ffffff00 26%, #ffffff1f 50%, #ffffff00 74%),
    linear-gradient(115deg, #c9a8ff00 0%, #c9a8ff21 22%, #a8d8ff21 42%, #a8f0d821 60%, #ffc9ec21 80%, #c9a8ff00 100%);
  background-size: 260% 260%, 340% 340%;
  background-position: var(--holo-x, 50%) var(--holo-y, 50%), var(--holo-x, 50%) var(--holo-y, 50%);
}
/* The same trick .p-holo uses on the crown, and for the same reason: a white
   tick on a green that lightens under the streak loses contrast exactly where
   the sheen is brightest. A dark rim under the stroke holds it there, so the
   mark never depends on how the phone happens to be held. Sub-pixel blur - at
   20px anything larger stops reading as a rim and starts reading as a shadow. */
body.peppy-theme .p-holo-dose svg { filter: drop-shadow(0 .5px 1.1px #0a5330a8); }
@media (prefers-reduced-motion: reduce) {
  body.peppy-theme .p-holo-dose::before { background-position: 50% 50%, 50% 50%; }
}

/* No tilt data and no mouse: the gradient still sits there as a static gold,
   which is the honest fallback rather than an animation running on a timer. */
@media (prefers-reduced-motion: reduce) {
  body.peppy-theme .p-holo { background-position: 50% 50%, 50% 50%; }
}

body.peppy-theme a:focus, body.peppy-theme button:focus { outline: none; }
/* Keyboard users still get a ring. */
body.peppy-theme a:focus-visible,
body.peppy-theme button:focus-visible,
body.peppy-theme [role="button"]:focus-visible {
  outline: 2px solid #0a9e8b;
  outline-offset: 2px;
}

/* --------------------------------------------------- tab bar motion -- */

.p-tab {
  transition: background-color .22s cubic-bezier(.22, .61, .36, 1),
              color .22s cubic-bezier(.22, .61, .36, 1);
}
body.peppy-theme .p-tab svg,
body.peppy-theme .p-tab span { transition: transform .22s cubic-bezier(.34, 1.4, .64, 1); }
/* The active tab lifts its icon slightly - enough to read as a change. */
body.peppy-theme .p-tab.is-active svg { transform: translateY(-1px) scale(1.08); }
.p-tab:active { transform: scale(.94); transition: transform .1s ease-out; }

@media (prefers-reduced-motion: reduce) {
  .p-tab, .p-tab svg, .p-tab span { transition: none; }
  .p-tab.is-active svg { transform: none; }
}

/* Tab icons: the design draws a solid glyph when active and an outline when
   not, so both are in the markup and swapped here rather than recoloured. */
body.peppy-theme .p-tab-icon { position: relative; display: block; width: 22px; height: 22px; }
body.peppy-theme .p-tab-icon svg { position: absolute; inset: 0; width: 22px; height: 22px; transition: opacity .18s ease; }
body.peppy-theme .p-tab-icon svg:last-child { opacity: 0; }
body.peppy-theme .p-tab.is-active .p-tab-icon svg:first-child { opacity: 0; }
body.peppy-theme .p-tab.is-active .p-tab-icon svg:last-child { opacity: 1; }
body.peppy-theme .p-tab.is-active .p-tab-icon { transform: translateY(-1px) scale(1.08); }
body.peppy-theme .p-tab-icon { transition: transform .22s cubic-bezier(.34, 1.4, .64, 1); }
@media (prefers-reduced-motion: reduce) {
  body.peppy-theme .p-tab-icon, body.peppy-theme .p-tab-icon svg { transition: none; }
  body.peppy-theme .p-tab.is-active .p-tab-icon { transform: none; }
}

/* The compound record's evidence snapshot ellipsised its own value: legacy CSS
   pins the label to one line, and "Insufficient evidence" needs 99px in a 96px
   column, so it read as "Insufficient evide...". An evidence level is the one
   thing on that card that must never be half-shown - let it wrap. */
body.peppy-theme .evidence-snapshot strong {
  overflow: visible;
  white-space: normal;
  text-overflow: clip;
  line-height: 1.25;
}

/* A sub-page on its way out must not intercept the tap that follows it. It
   still overlaps the row behind for the first part of the slide, which read as
   "the first tap does nothing". Only `is-closing` is inert - `is-settling` also
   marks a swipe that sprang back and is staying open. */
body.peppy-theme .p-subpage.is-closing { pointer-events: none; }

/* ------------------------------------------------ page titles scroll -- */

/* The design gives every screen a fixed header above a scrolling body, which
   left the title floating while the content moved under it. The title belongs
   to the page, so the whole column scrolls as one: the view (or sub-page)
   becomes the scroll container and its children simply stack inside it.
   `!important` is needed only because the design markup carries the old
   overflow on inline style attributes, which a stylesheet cannot otherwise
   outrank. Both axes must be released together - a box with one axis visible
   and the other clipped resolves the visible one back to auto. */
body.peppy-theme .app-view.active,
body.peppy-theme .p-subpage {
  overflow-y: auto;
  overflow-x: hidden;
  scrollbar-width: none;
}
/* Sideways belongs to the swipe, not to the browser.

   Without this the page inherits `manipulation` from .p-shell, which lets the
   compositor treat a drag as a scroll in either axis - and the moment it does,
   it fires pointercancel and stops sending pointermove, so the handler never
   sees the gesture end. A real thumb is never perfectly horizontal, so it lost
   every swipe. Synthetic pointer events skip the compositor entirely, which is
   why this passed in a browser and failed on the phone.

   `pan-y` reserves the horizontal for JS while leaving vertical scrolling
   native. `pinch-zoom` is kept alongside it deliberately: .p-shell chose
   `manipulation` to preserve magnification, and dropping to bare `pan-y` here
   would quietly take that away from every main page. The sub-page above has
   used pan-y since its own swipe-to-dismiss was written, which is why that one
   has always worked on a device. */
body.peppy-theme .app-view.active { touch-action: pan-y pinch-zoom; }

/* The bar changes with the page rather than ahead of it. A swipe moves the
   whole screen, and a highlight that snapped while the page was still easing
   in was the one part of the change that looked abrupt. Matched to the view
   animation's own timing so the two land together. */
body.peppy-theme .p-tab {
  transition: background-color .3s cubic-bezier(.22, .68, .24, 1),
              color .3s cubic-bezier(.22, .68, .24, 1);
}
body.peppy-no-motion .p-tab { transition: none; }
body.peppy-theme .app-view.active::-webkit-scrollbar,
body.peppy-theme .p-subpage::-webkit-scrollbar { width: 0; height: 0; display: none; }

body.peppy-theme .app-view.active > *,
body.peppy-theme .p-subpage > * {
  flex: 0 0 auto !important;
  min-height: auto !important;
  overflow: visible !important;
}

/* ------------------------------------------------------- system bars -- */

/* Android draws edge-to-edge under Capacitor, so the status and navigation
   bars sit on top of the app rather than beside it. Capacitor's SystemBars
   plugin injects --safe-area-inset-*; iOS and the browser have env() instead,
   and a plain browser has neither - hence the two-step fallback.
   Padding on the shell rather than on each screen: one rule insets every
   screen at once, and the shell's own background then paints behind both bars.
   The floating tab bar needs its own inset regardless - an absolutely
   positioned child resolves `bottom: 0` against the padding box, whose edge is
   below the padding, so shell padding alone does not lift it. */
:root {
  --p-safe-top: var(--safe-area-inset-top, env(safe-area-inset-top, 0px));
  --p-safe-right: var(--safe-area-inset-right, env(safe-area-inset-right, 0px));
  --p-safe-bottom: var(--safe-area-inset-bottom, env(safe-area-inset-bottom, 0px));
  --p-safe-left: var(--safe-area-inset-left, env(safe-area-inset-left, 0px));
}
body.peppy-theme .p-shell {
  box-sizing: border-box;
  padding-top: var(--p-safe-top);
  padding-bottom: var(--p-safe-bottom);
  padding-left: var(--p-safe-left);
  padding-right: var(--p-safe-right);
}

/* Lift the floating bar clear of the gesture bar. 12px is the design's own
   bottom padding; the inset is added to it rather than replacing it. */
body.peppy-theme .p-tabbar {
  padding-bottom: calc(12px + var(--p-safe-bottom));
}

/* ------------------------------------------------------ tap gestures -- */

/* The viewport permits zoom, so Chrome keeps double-tap-to-zoom armed and will
   not commit a tap to a click until it knows no second tap is coming. Two taps
   in quick succession - closing a sub-page, then choosing the next thing - get
   claimed as that gesture, and the browser dispatches the full touch sequence
   with no click at all. On-device logging confirmed exactly that: identical
   touchstart/touchend either way, click present only when the tap worked.

   `manipulation` gives up double-tap zoom and keeps panning and pinch zoom, so
   nobody loses the ability to magnify the page. touch-action is resolved across
   the ancestor chain, so declaring it on the shell covers everything inside;
   the sub-page's own pan-y is stricter and still wins where it applies. */
body.peppy-theme .p-shell,
body.peppy-theme .p-tab,
body.peppy-theme [data-more-action],
body.peppy-theme [data-view-link],
body.peppy-theme [data-subpage-back],
body.peppy-theme button,
body.peppy-theme a,
body.peppy-theme [role="button"] {
  touch-action: manipulation;
}

/* The library list is still carrying a legacy multi-column layout
   (`display:block; column-count:1`), and `gap` does nothing on a block box, so
   every card sat flush against the next. A plain column stack with the same
   12px rhythm the rest of the theme uses. !important only to outrank the
   legacy declarations, which are themselves !important. */
body.peppy-theme .compound-cards {
  display: flex !important;
  flex-direction: column;
  gap: 12px !important;
  column-count: auto !important;
  column-gap: normal !important;
}

/* ------------------------------------------------------- appearance -- */

/* Text size rides on the scrolling area rather than the shell, so the tab bar
   and page header keep their size and only the content grows - larger content
   just scrolls further, instead of pushing the frame wider than the screen. */
body.peppy-theme .app-view.active { zoom: var(--p-text-scale, 1); }
/* Sub-pages scale on their children, not on themselves: .p-subpage is absolutely
   positioned at inset 0, so zooming the container would scale that geometry too
   and push it past the shell. */
body.peppy-theme .p-subpage > * { zoom: var(--p-text-scale, 1); }

/* Motion off: the mascot's idle bob and the sub-page slide both stop. Kept
   separate from prefers-reduced-motion so the system setting still wins on its
   own, without the app having to opt in. */
body.peppy-no-motion *,
body.peppy-no-motion *::before,
body.peppy-no-motion *::after {
  animation: none !important;
  transition: none !important;
}

/* --------------------------------------------- cards have no outline -- */

/* The design puts `border:1.5px solid #hex` inline on roughly eighty elements,
   and an inline declaration cannot be reached from here without !important -
   which is the same reason scripts/build-dark-theme.mjs generates its overrides
   that way. This is that file's light-theme counterpart, written by hand
   because there is no generator on this side.

   Only the four-sided shorthand. `border-top` is how a rule *inside* a card is
   drawn - a separator, not an outline - and those are left alone. So is
   `border:2px`, which is the Appearance swatch marking the theme you are on:
   that outline is information, not decoration.

   The width stays and only the colour goes, so switching this block off changes
   nothing but the colour and nothing reflows either way. */
body.peppy-theme [style*="border:1.5px solid"],
body.peppy-theme [style*="border:1px solid"] {
  border-color: transparent !important;
}

/* The tile shadow, on the elements that carried the outline. Raised for the
   reason the variable above gives: it is now the edge. Matched on the shadow
   the design actually writes inline, so an element that never had one does not
   grow one - a pill is bordered too, and a pill with a drop shadow is wrong. */
body.peppy-theme:not(.peppy-dark) [style*="box-shadow:0 5px 14px #16866e0d"] {
  box-shadow: 0 5px 16px #16866e1f !important;
}

/* ------------------------------------------------------- dark theme -- */

/* Two layers make up the dark theme. peppy-dark.css is generated and covers the
   inline colours the design puts on individual elements; this section covers the
   class-based components defined above, which a generator scanning inline styles
   cannot see. Hand-written on purpose - these are the shared surfaces, and they
   set the tone the generated rules sit inside. */

body.peppy-theme.peppy-dark { background: #0b100f; color: #d6e4e0; }

/* !important only to outrank peppy.css's own `.app-shell{background:#fff!important}`.
   This is the surface the Android system bars show through under edge-to-edge,
   so leaving it white would keep the bars white however the theme is set. */
body.peppy-dark .p-shell { background: #111716; }
body.peppy-dark .content { background: transparent; }

body.peppy-dark .p-head h1 { color: #7fd8c6; }
body.peppy-dark .p-head p,
body.peppy-dark .p-section-head h2 { color: #9db0ac; }
body.peppy-dark .p-eyebrow { color: #6fbfae; }
/* The banner's own paragraph colour is #5c7b8a, which on this background
   measures 4.02:1 - readable, and under the 4.5 body text needs. It is set on a
   class rather than inline, so the generated layer never saw it. */
body.peppy-dark .p-banner p { color: #a3b6b2; }

body.peppy-dark .p-tabbar-inner { background: #19211f; box-shadow: 0 8px 24px #0006; }
body.peppy-dark .p-tab { color: #8fa39f; }
body.peppy-dark .p-tab.is-active { background: #12312c; color: #5fd3bd; }

/* No outline in dark. A pale hairline is how the light theme separates a white
   card from a near-white page; here the card is already lighter than what is
   behind it, so the outline is a ring the design does not need and a column of
   them reads as clutter. The shadow separates them instead. Transparent rather
   than `border:0`, so the 1.5px keeps its space and switching theme does not
   reflow the page. */
body.peppy-dark .p-card,
body.peppy-dark .p-row {
  background: #19211f;
  border-color: transparent;
  box-shadow: 0 8px 20px #0005;
}
body.peppy-dark .p-row strong { color: #e2efeb; }
body.peppy-dark .p-row span { color: #9db0ac; }
body.peppy-dark .p-row b { color: #5f7570; }

body.peppy-dark .p-chip {
  background: #19211f;
  border-color: #2c3835;
  color: #b9cbc6;
}
body.peppy-dark .p-pill-link { background: #12312c; color: #5fd3bd; }

body.peppy-dark .p-search { background: #1b2422; color: #7d918d; }
body.peppy-dark .p-search:focus-within { background: #212c29; }
body.peppy-dark .p-search input { color: #e2efeb; }
body.peppy-dark .p-search input::placeholder { color: #7d918d; }

body.peppy-dark .p-meta,
body.peppy-dark .p-meta span { color: #8fa39f; }
body.peppy-dark .p-disclaimer { background: #221d12; color: #c8ab74; }

body.peppy-dark .p-segment {
  background: #19211f;
  border-color: #2c3835;
  color: #b9cbc6;
}
body.peppy-dark .p-segment.is-active { background: #12312c; color: #5fd3bd; border-color: #1c4b43; }

body.peppy-dark .p-subpage { background: #111716; }
body.peppy-dark .p-subpage::before { background: linear-gradient(90deg, transparent, #0006); }

/* The mascot art is drawn for a light page. Dimming it slightly stops it
   glowing out of a dark screen without making it look washed out. */
body.peppy-dark .p-shell img { filter: brightness(.92) saturate(.95); }

/* ------------------------------------------- compound record layout -- */

/* Each tab used to be a single panel holding several eyebrow-and-paragraph
   pairs, so the sections ran together and a label sat hard against the end of
   the paragraph above it. Every block is its own section now; these rules give
   them room and a card of their own. No text was removed - it is the same
   content, just separated so the eye can find the parts. */
/* .active only: a bare .compound-view rule outranks the `display:none` that
   hides the inactive tabs, and every tab's content shows at once. */
body.peppy-theme .compound-view.active {
  display: flex;
  flex-direction: column;
  gap: 12px;
}

/* Overview and Details had no card treatment at all. Research and Interactions
   keep the amber and mint they already use, so each tab stays recognisable. */
body.peppy-theme .compound-view[data-compound-view="overview"] .reference-section,
body.peppy-theme .compound-view[data-compound-view="details"] .reference-section {
  padding: 14px 15px !important;
  border: 1px solid #e8f2ef !important;
  border-radius: 18px !important;
  background: #f7fbfa !important;
}

/* The label needs to breathe under its own heading, not just above it. */
body.peppy-theme .compound-view .eyebrow { margin: 0 0 8px !important; letter-spacing: .09em !important; }

/* Citations were a cramped bullet list of underlined links. Spaced out, with
   the marker outside the text block so wrapped lines stay aligned. */
body.peppy-theme .compound-view .reference-section ul {
  margin: 0;
  padding-left: 18px;
  display: flex;
  flex-direction: column;
  gap: 10px;
}

/* The record's closing disclaimer. It follows up to four amber section cards on
   the Research tab, so another full-width amber block read as one more section
   rather than as a footnote. Quieter, centred, and set apart by a short rule so
   it is clearly the end of the record and not part of it. */
body.peppy-theme #compound-dialog .form-disclaimer {
  /* A full-bleed band closing the record: square corners of its own, with the
     bottom two taken from the dialog, which clips to its 28px radius. A rule
     along the top separates it from the last section instead of a border box. */
  margin: 14px 0 0;
  padding: 14px 20px 16px;
  border: 0;
  border-top: 1px solid #f0e4cd;
  border-radius: 0;
  background: #fffdf8;
  color: #97794a;
  font-size: 11px;
  font-weight: 700;
  line-height: 1.55;
  text-align: center;
  /* Even line lengths - without it the last line often drops a single word. */
  text-wrap: balance;
}

/* ----------------------------------------------- compound drawer (8a) -- */

/* The record is a sheet that rises from the bottom of the frame. It sits
   outside .p-shell, which clips its children, and is constrained to the same
   430px column so it lines up with the app on a wide screen. */
.p-drawer-backdrop {
  position: fixed;
  inset: 0;
  z-index: 60;
  background: #0b201c;
  opacity: 0;
  transition: opacity .28s ease;
}
.p-drawer-backdrop.is-open { opacity: .42; }

.p-drawer {
  position: fixed;
  z-index: 61;
  /* Centred with auto margins rather than left:50% + translate(-50%): on an odd
     viewport width those resolve to fractional pixels and leave a hairline gap
     down one side. It also keeps the drag transform to a single axis. */
  left: 0;
  right: 0;
  margin: 0 auto;
  /* A fixed sheet, not one that sizes to its content: switching between a short
     tab and a long one would otherwise resize the whole drawer under the
     reader. Flush to the bottom of the screen, so only the top corners round. */
  top: calc(26px + var(--p-safe-top));
  bottom: 0;
  width: 100%;
  max-width: var(--p-shell-w);
  display: flex;
  flex-direction: column;
  border-radius: 30px 30px 0 0;
  background: #f4fbf9;
  box-shadow: 0 26px 60px #1434301f, 0 -8px 40px #14343014;
  overflow: hidden;
  /* Off-screen until opened; the -50% keeps it centred while translating.
     translate3d keeps the sheet on its own compositing layer so a drag moves
     it without repainting the content underneath. */
  transform: translate3d(0, 100%, 0);
  transition: transform .34s cubic-bezier(.32, .72, 0, 1);
  /* none, not pan-y: pan-y hands vertical gestures to the browser, which then
     stops delivering pointermove and the sheet cannot be dragged at all. The
     scrolling body opts back in below. */
  touch-action: none;
}

/* The content scrolls natively; contain stops that scroll chaining out to the
   page behind once it reaches an end. */
.p-drawer [data-drawer-body] {
  touch-action: pan-y;
  overscroll-behavior: contain;
}
.p-drawer.is-open { transform: translate3d(0, 0, 0); }
/* While a finger is on it the sheet follows exactly, with no transition to lag
   behind the touch; will-change keeps the layer promoted for the whole gesture
   rather than re-promoting on every frame. */
.p-drawer.is-dragging { transition: none; will-change: transform; }
.p-drawer[hidden] { display: none; }

/* Clear of the gesture bar, and of the floating tab bar it covers. */
.p-drawer [data-drawer-body] { padding-bottom: calc(20px + var(--p-safe-bottom)); }

@media (prefers-reduced-motion: reduce) {
  .p-drawer, .p-drawer-backdrop { transition: none; }
}

/* ------------------------------------------------ compound tab motion -- */

/* The pill slides between tabs rather than jumping. This only works because
   peppy-compound.js's paintTab() updates these buttons in place - a full
   re-render replaces them, and a brand new element has nothing to transition
   from. */
body.peppy-theme [data-compound-tab] {
  transition: background-color .22s ease, color .22s ease, box-shadow .22s ease;
}

/* And the content arrives from the side the swipe came from, so the gesture and
   the thing it moved agree about which way is forward. Short and small: this
   runs on every tab tap as well as every swipe, and anything longer starts to
   feel like waiting. */
@keyframes p-tab-forward {
  from { opacity: 0; transform: translate3d(16px, 0, 0); }
  to   { opacity: 1; transform: none; }
}
@keyframes p-tab-back {
  from { opacity: 0; transform: translate3d(-16px, 0, 0); }
  to   { opacity: 1; transform: none; }
}
body.peppy-theme [data-drawer-body].is-tab-forward { animation: p-tab-forward .24s cubic-bezier(.2, .7, .3, 1) both; }
body.peppy-theme [data-drawer-body].is-tab-back { animation: p-tab-back .24s cubic-bezier(.2, .7, .3, 1) both; }

/* peppy-no-motion already kills the keyframes; the pill needs saying
   separately, because a transition is not an animation. */
body.peppy-no-motion [data-compound-tab] { transition: none; }

/* -------------------------------------------------- save mark motion -- */

.p-save { display: block; line-height: 1; }
/* The button sets font-size, and the icon is drawn at 1em, so both states and
   both torn halves size themselves from it. */
.p-save svg, .p-save-half svg { display: block; }

/* Saving: a heart too big for the button drops in and settles to size, with a
   small overshoot so it lands rather than simply stopping. */
.p-save.is-falling { animation: p-save-fall .46s cubic-bezier(.34, 1.32, .5, 1) both; }
@keyframes p-save-fall {
  0%   { transform: translateY(-13px) scale(2.7); opacity: 0; }
  40%  { opacity: 1; }
  68%  { transform: translateY(0) scale(.86); }
  84%  { transform: translateY(0) scale(1.09); }
  100% { transform: translateY(0) scale(1); opacity: 1; }
}

/* Unsaving: two copies of the glyph, each clipped to one side, tip apart and
   fall. Clipping a text character is what sells the break - there is no
   half-heart glyph to use. */
.p-save-half {
  position: absolute;
  inset: 0;
  display: grid;
  place-items: center;
  color: #e9829a;
  line-height: 1;
  pointer-events: none;
}
.p-save-half.is-left {
  clip-path: inset(0 50% 0 0);
  animation: p-save-tear-left .42s cubic-bezier(.4, 0, .7, 1) both;
}
.p-save-half.is-right {
  clip-path: inset(0 0 0 50%);
  animation: p-save-tear-right .42s cubic-bezier(.4, 0, .7, 1) both;
}
/* A beat of recoil before they part, so it reads as breaking rather than
   simply sliding away. */
@keyframes p-save-tear-left {
  0%   { transform: translate(0, 0) rotate(0); opacity: 1; }
  18%  { transform: translate(1px, -2px) rotate(4deg); }
  100% { transform: translate(-11px, 15px) rotate(-34deg); opacity: 0; }
}
@keyframes p-save-tear-right {
  0%   { transform: translate(0, 0) rotate(0); opacity: 1; }
  18%  { transform: translate(-1px, -2px) rotate(-4deg); }
  100% { transform: translate(11px, 15px) rotate(34deg); opacity: 0; }
}

/* The app's own motion switch and the system setting both skip it; the JS
   checks the same conditions and paints the end state directly. */
@media (prefers-reduced-motion: reduce) {
  .p-save.is-falling, .p-save-half { animation: none; }
}

/* The save heart sits inline in the card header now, beside the name. The
   legacy rules still pin it into the card's top-right corner for the old
   layout, and that `top` leaked through the moment the button was given a
   positioning context for the break animation - dropping it out of line with
   the category icon. Positioning belongs to the flex row it lives in. */
/* --- library view toggle --------------------------------------------------- */

/* The search field and the toggle on one row. The field takes the space and the
   toggle keeps its own width, so a long placeholder cannot push the buttons off
   the edge. */
body.peppy-theme .p-library-tools { flex: none; display: flex; align-items: stretch; gap: 8px; min-width: 0; }
body.peppy-theme .p-library-tools .p-search { flex: 1 1 auto; min-width: 0; }
body.peppy-theme .p-view-toggle {
  flex: none;
  display: flex;
  align-items: center;
  gap: 3px;
  padding: 3px;
  border-radius: 16px;
  background: var(--p-surface);
  box-shadow: 0 5px 14px #16866e0d;
}
body.peppy-theme .p-view-toggle button {
  display: grid;
  place-items: center;
  width: 38px;
  height: 38px;
  padding: 0;
  border: 0;
  border-radius: 13px;
  background: none;
  color: #7f948f;
  cursor: pointer;
  transition: background .16s ease, color .16s ease;
}
/* aria-pressed carries the state, so the styling reads from it rather than from
   a class kept in step by hand - the button cannot look selected without also
   telling a screen reader that it is. */
body.peppy-theme .p-view-toggle button[aria-pressed="true"] {
  background: var(--p-mint);
  color: var(--p-teal-deep);
}
@media (hover: hover) {
  body.peppy-theme .p-view-toggle button:not([aria-pressed="true"]):hover { background: var(--p-mint-faint); color: var(--p-teal-deep); }
}

body.peppy-theme .compound-favourite {
  position: relative;
  top: auto;
  right: auto;
  align-self: center;
}

/* Matches the library card's h3 so the two headers read as one component. The
   legacy `h2{font-size:18px!important}` that this used to fight has been
   removed, so it no longer needs !important to win. */
body.peppy-theme .p-drawer h2 {
  font-size: 19px;
  font-weight: 900;
}
