/* my.css — /p/my dashboard (myJASPER). Design source: Figma 192:3.
   Mobile-first (390 frame); fluid >=768 per MOBILE_STRATEGY §B2. Uses tokens.css. */

* { box-sizing: border-box; }
body { margin: 0; background: var(--space-gradient), var(--bg-primary); background-repeat: no-repeat; background-attachment: fixed; color: var(--text-primary); font-family: var(--font-thai); }

/* Starfield CSS (#p-starfield/.p-star/etc.) now lives in tokens.css
   (2026-07-10 — moved so every /p page can share it, not just this one). */

/* Page content must be an explicitly positioned/z-indexed stacking context
   to paint above the fixed z-index:0 starfield layers (same pattern the main
   site uses for its #page-layout) — plain static-position content would
   otherwise paint BEHIND a positioned z-index:0 sibling per CSS stacking
   rules, hiding the whole page under the stars. */
/* Subtracts --navbar-h (2026-07-08 fix): nav.js prepends the shared navbar
   directly to <body>, as a SEPARATE sibling before this page's own root —
   not inside it — so without the subtraction, total body height was always
   navbar-h taller than the viewport, forcing a redundant scroll on every
   load even when the dashboard's own content fit on one screen. dvh (not
   vh) also accounts for mobile browsers' collapsible address bar. */
/* display:flex column (2026-07-10) — lets the footer pin to the bottom of
   the viewport via margin-top:auto below when the dashboard is short
   (2026-08-16: the dashboard's lists — tickets, timeline, etc. — grow over
   time, so .my-content itself stays top-pinned rather than centered; only
   the footer chases the bottom). This also governs /p/my/tickets — that
   page's root carries this same .my-page class (see tickets.css). */
.my-page { position: relative; z-index: 1; display: flex; flex-direction: column; min-height: calc(100vh - var(--navbar-total-h)); min-height: calc(100dvh - var(--navbar-total-h)); }
.my-content { max-width: 720px; padding: 16px; display: flex; flex-direction: column; gap: 16px; }

/* Wraps the A2HS banner — a plain sibling of .my-content, appended straight
   to .my-page (same pattern as the legal footer just below it). Its actual
   width is set inline by my.js (ResizeObserver synced to the Concert
   Timeline card, 2026-08-14 ask) so it always matches that column exactly
   at every breakpoint; max-width/margin here are only the pre-JS fallback
   for the first paint before the observer's first callback fires.
   No left/right padding (2026-08-14 fix) — .a2hs-banner fills 100% of this
   wrap's content box, so side padding here shrank the VISIBLE card 32px
   narrower than the wrap itself, which is what my.js actually measures
   against the Timeline card — the wrap matched, but the card people could
   see didn't. Bottom padding only, for spacing above the legal footer. */
.a2hs-wrap { max-width: 720px; margin: 0 auto; padding: 0 0 16px; }

/* Legal footer — appended as a sibling AFTER .my-content closes. margin-top:
   auto (2026-07-10) pins it to the bottom of the viewport when content is
   short, replacing the old fixed/negative margin-top tuning that only
   mattered for the overflow case. */
.my-page .lg-footer-link { margin-top: auto; padding-bottom: 20px; }

/* Layout groups — on mobile these are transparent (display:contents), so the
   section order/spacing is identical to a single flex column. */
.my-group, .my-bento-row, .my-bento-col { display: contents; }

/* Fan Benefits / Reminders patch slots (2026-07-11 sectional loading) — pure
   DOM anchors for patchFanBenefits() to swap contents into later; always
   display:contents (not just mobile) so they never introduce a layout box
   of their own — their children participate directly in the parent flex
   container's gap, exactly as if the slot wrapper didn't exist. */
.my-benefits-slot, .my-reminders-slot { display: contents; }

/* ── Desktop bento layout (MOBILE_STRATEGY §B2 breakpoint: 768px) ────────── */
@media (min-width: 768px) {
  .my-content {
    max-width: 1400px;
    margin: 0 auto;
    padding: 28px 24px;
    display: grid;
    /* Sidebar is a proportional %, clamped so it never gets unreadably
       narrow or unreasonably wide as the window resizes. (299px/30% = +15%) */
    grid-template-columns: minmax(299px, 30%) 1fr;
    align-items: start;
    gap: 24px;
  }
  .my-group--left {
    display: flex; flex-direction: column; gap: 16px;
    position: sticky; top: 24px;
    container-type: inline-size;   /* lets wallet cards react to actual column width */
  }
  /* Sidebar too cramped for the peek price chip — hide it there only.
     The expanded detail's price (.my-meta__price) is a different element
     and is never affected. */
  @container (max-width: 320px) {
    .my-ticket__price { display: none; }
  }
  .my-group--right { display: flex; flex-direction: column; gap: 24px; }
  .my-bento-col { display: flex; flex-direction: column; gap: 16px; min-width: 0; }
  /* Divider above Fan Benefits is redundant once columns visually separate it */
  .my-divider--benefits { display: none; }

  /* 768–1099px: sidebar + one stacked content column = 2 columns total.
     MD and Timeline stack (not enough room to sit side by side). */
  .my-bento-row { display: grid; grid-template-columns: 1fr; gap: 24px; }
}

/* >=1100px: enough room for sidebar + MD + Timeline side by side = 3 columns. */
@media (min-width: 1100px) {
  .my-content { grid-template-columns: minmax(322px, 25%) 1fr; }   /* 280px/22% +15% */
  .my-bento-row { grid-template-columns: 1fr 1fr; }
}

.my-state { padding: 48px 16px; text-align: center; color: var(--text-secondary); font-size: 13px; }
.my-state__link { display: inline-block; margin-top: 6px; color: var(--accent-gold-text); text-decoration: underline; cursor: pointer; }

/* Shown instead of the dashboard when the member has 0 tickets — centered
   dead in the middle of the page (not just top-of-page). */
.my-empty-wrap {
  min-height: calc(100vh - var(--navbar-total-h));
  min-height: calc(100dvh - var(--navbar-total-h));
  display: flex; align-items: center; justify-content: center;
  padding: 24px;
}
.my-empty-tickets {
  display: flex; flex-direction: column; align-items: center; gap: 14px;
  text-align: center; padding: 48px 36px; max-width: 420px;
  background:
    radial-gradient(circle at 50% 0%, rgba(var(--accent-gold-rgb),0.16), transparent 60%),
    var(--bg-secondary);
  border: 1px solid var(--border);
  border-radius: var(--radius-5xl);
  box-shadow: 0 24px 60px rgba(var(--shadow-rgb), calc(0.35 * var(--shadow-mult)));
}
.my-empty-tickets__icon {
  width: 84px; height: 84px;
  display: flex; align-items: center; justify-content: center;
  border-radius: var(--radius-full);
  font-size: 42px;
  background: rgba(var(--accent-gold-rgb),0.14);
  box-shadow: 0 0 34px rgba(var(--accent-gold-rgb),0.28);
}
.my-empty-tickets__title { font-size: 21px; font-weight: 800; color: var(--text-primary); }
.my-empty-tickets__msg { font-size: 14px; color: var(--text-secondary); line-height: 1.6; }
.my-empty-tickets__cta {
  margin-top: 6px;
  display: inline-flex; align-items: center; gap: 6px;
  padding: 13px 26px;
  border: none; border-radius: var(--radius-xl);
  /* Fixed gradient, not theme-following (2026-08-08 fix) — same drift-from-
     reference bug as p/homesafe.css's .hs-auth-gate__cta. */
  background: linear-gradient(120deg, var(--cta-grad-hi), var(--cta-grad-lo));
  color: var(--brand-gold-ink);
  font: 700 14px var(--font-thai);
  text-decoration: none;
  cursor: pointer;
  box-shadow: 0 10px 24px rgba(var(--accent-gold-rgb),0.3), inset 0 1px 0 rgba(255, 255, 255, .18);
  transition: filter 0.15s ease;
}
@media (hover: hover) { .my-empty-tickets__cta:hover { filter: brightness(1.06); } }

/* Section divider — inserted before each new section heading. Constellation
   style: a soft center glow fading to transparent at both ends, plus a
   single star dot in the middle, instead of a flat uniform line. */
.my-divider {
  position: relative;
  height: 0;
  margin: 6px 0;
  border-top: 1px solid transparent;
  border-image: linear-gradient(90deg, transparent, var(--border) 20%, rgba(var(--accent-teal-rgb),0.35) 50%, var(--border) 80%, transparent) 1;
}
.my-divider::after {
  content: '';
  position: absolute;
  left: 50%; top: 50%;
  width: 3px; height: 3px;
  border-radius: var(--radius-full);
  background: var(--accent-teal);
  box-shadow: 0 0 6px 1px rgba(var(--accent-teal-rgb),0.7);
  transform: translate(-50%, -50%);
}
:root[data-theme="light"] .my-divider::after { background: var(--brand-teal); }

/* Greeting */
.my-greet { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
.my-greet__avatar {
  position: relative;
  width: 40px; height: 40px; flex-shrink: 0;
  display: flex; align-items: center; justify-content: center;
  border-radius: var(--radius-full); font-size: 21px;   /* 18px +15% */
  background: var(--bg-elevated); border: 3px solid var(--accent-teal);
}
/* Slow-spinning dashed "orbit ring" around the avatar — purely decorative,
   sits just outside the avatar's own border, tinted to the user's identity
   color (falls back to teal like the avatar border itself). */
.my-greet__avatar::before {
  content: '';
  position: absolute;
  inset: -6px;
  border-radius: var(--radius-full);
  border: 1px dashed color-mix(in srgb, var(--avatar-color, var(--accent-teal)) 55%, transparent);
  animation: p-orbit-spin 14s linear infinite;
}
@keyframes p-orbit-spin { to { transform: rotate(360deg); } }
.my-greet__title { font-size: 18px; font-weight: 700; }   /* 16px +15% */
.my-greet__sub { font-size: 11px; color: var(--text-secondary); }

/* Edit Profile button — tinted to the user's own identity color (set inline
   via JS from profile.color). Sits inline (top-right) on mobile.
   Desktop layout (via `order` + a flex-wrap break): row 1 = avatar (left) +
   button (right, pushed by margin-left:auto); row 2 = greeting text, filling
   the space below the avatar. */
.my-edit-profile-btn {
  display: inline-flex; align-items: center; gap: 4px;
  align-self: flex-start;
  margin-left: auto;
  padding: 5px 13px;         /* 6px/14px -10% */
  border: none;
  border-radius: var(--radius-pill);
  background: rgba(var(--accent-gold-rgb),0.14);
  color: var(--accent-gold-text);
  font: 600 11px var(--font-thai);   /* 12px -10% */
  cursor: pointer;
  white-space: nowrap;
}
/* Forces a flex-wrap line break right after the button — width:0 keeps it
   from taking visual space; only active at the desktop breakpoint. */
/* Row 1 = avatar + edit button, row 2 = greeting text (full width) — same
   layout on mobile as desktop (2026-08-08 ask) since long usernames wrapped
   awkwardly next to the avatar/button when this break was desktop-only. */
.my-greet__break { flex-basis: 100%; width: 0; height: 0; display: block; order: 2; }
.my-greet__avatar { order: 0; }
.my-edit-profile-btn { order: 1; margin-left: auto; }
.my-greet__text { order: 3; flex-basis: 100%; }

/* Card base — subtle nebula glow ties every card into the same space theme
   as the lucky-draw hero cards / reminders card (previously only those two
   had any gradient; plain cards looked like a generic dashboard). */
.my-card {
  position: relative;
  background:
    radial-gradient(ellipse 260px 140px at 100% 0%, rgba(var(--accent-teal-rgb),0.07), transparent 65%),
    radial-gradient(ellipse 220px 160px at 0% 100%, rgba(var(--accent-gold-rgb),0.05), transparent 65%),
    var(--bg-secondary);
  border: 1px solid var(--border);
  border-radius: var(--radius-2xl);
  padding: 14px 16px;
}

/* Budget */
.my-budget__head { display: flex; justify-content: space-between; align-items: center; }
.my-budget__label { font-size: 13px; font-weight: 700; color: var(--text-secondary); }
.my-budget__edit { font-size: 12px; color: var(--accent-gold-text); cursor: pointer; text-decoration: none; }
.my-budget__total {
  font-size: 34px; font-weight: 700; margin: 0 0 8px;   /* 30px +15% */
  color: var(--accent-gold-text);   /* was PPW mint #2EE6C5 (2026-10-22) */
  text-shadow: 0 0 18px rgba(var(--accent-gold-rgb), 0.45);
}
/* Light mode (2026-08-07 ask: "ปรับสีข้อความและเงาให้เห็นชัดเจน ... แต่
   ยังสดใสอยู่") — the dark-mode teal-emerald measured 1.25:1 contrast
   against the light backgrounds, effectively invisible. Same hue/saturation,
   darkened to clear ~3.5:1 (the same floor used sitewide, see
   LIGHT_MODE_COLOR_BIBLE.md), not a token since this figure's color is
   unique to this one element. */
:root[data-theme="light"] .my-budget__total {
  color: var(--accent-gold);   /* was PPW #108772 (2026-10-22) */
  text-shadow: 0 0 18px rgba(var(--accent-gold-rgb), 0.3);
}
.my-budget__hr { height: 0; border-top: 1px solid var(--border); margin: 8px 0; }
.my-budget__row { display: flex; justify-content: space-between; font-size: 13px; padding: 3px 0; }
.my-budget__row-label { color: var(--text-secondary); }
.my-budget__note { font-size: 9.9px; color: var(--text-secondary); margin-top: 8px; }

/* Eye toggle (2026-07-09) — a normal flex item inside .my-budget__head
   (which is already justify-content:space-between), not absolutely
   positioned, so it always stays inside the card's own padding instead of
   floating past its edge. Teal-emerald tone (2026-07-09 follow-up) matches
   .my-budget__total's own color, tying the button visually to the figure
   it controls instead of the unrelated gold CTA tone. */
.my-budget__eye-btn {
  flex-shrink: 0;
  width: 30px; height: 30px;
  display: flex; align-items: center; justify-content: center;
  border-radius: var(--radius-full);
  background: linear-gradient(135deg, var(--cta-grad-hi), var(--cta-grad-lo));   /* was PPW mint (2026-10-22) */
  color: var(--brand-gold-ink);
  border: none;
  box-shadow: 0 3px 10px rgba(var(--accent-gold-rgb), 0.45);
  font-size: 14px;
  cursor: pointer;
  transition: filter 0.15s ease, transform 0.15s ease;
}
@media (hover: hover) { .my-budget__eye-btn:hover { filter: brightness(1.08); transform: scale(1.06); } }

/* Section header — scroll-margin-top (2026-07-27 fix) so the jump-nav's
   scrollIntoView() lands below the fixed navbar instead of tucking the
   section title underneath it. */
.my-sec-head { display: flex; justify-content: space-between; align-items: center; scroll-margin-top: calc(var(--navbar-total-h) + 12px); }
.my-sec-title { font-size: 17px; font-weight: 700; }   /* 15px +15% */
.my-sec-link { font-size: 12px; color: var(--accent-gold-text); cursor: pointer; text-decoration: none; }
button.my-sec-link { background: none; border: 0; padding: 0; font-family: inherit; }
.my-sec-icon-flat { display: inline-flex; vertical-align: -2px; color: var(--accent-gold-text); }
.my-bias-empty { font-size: 13px; color: var(--text-secondary); }
.my-hint { font-size: 9.72px; text-align: right; color: var(--text-secondary); }   /* 12px -10% -10% */

/* "ดูผังที่นั่งของฉัน" — jumps to Find My JASPER anchored on the primary ticket's
   zone (2026-07-08 ask). Sits between the wallet and the tap-hint line. */
.my-seatmap-btn {
  display: flex; align-items: center; justify-content: center; gap: 6px;
  margin-top: 0;
  padding: 10px 14px;
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  background: rgba(var(--surface-rgb),0.04);
  color: var(--text-primary);
  font: 600 13px var(--font-thai);
  text-decoration: none;
  cursor: pointer;
}
@media (hover: hover) { .my-seatmap-btn:hover { background: rgba(var(--surface-rgb),0.08); border-color: var(--accent-teal); } }

/* Wallet stack (ticket cards) — solid cards, gentle overlap, click to expand */
.my-wallet { display: flex; flex-direction: column; }

.my-ticket {
  position: relative;
  display: flex;
  background: var(--bg-secondary);
  border: 1px solid var(--border);
  /* Colored border/ring is reserved for verified tickets (--ticket-border is
     only set by JS when verified); everything else falls back to the same
     plain border every other card uses. */
  border-top: 2px solid var(--ticket-border, var(--border));
  border-radius: var(--radius-2xl);
  overflow: hidden;
  cursor: pointer;
  margin-top: -14px;                 /* wallet overlap */
  /* Layered shadow stack (2026-07-27, Feel Lab ref) — tight/dark + medium +
     wide/faint, instead of one flat shadow; reads with more depth at the
     same visual weight. */
  box-shadow:
    0 1px 3px rgba(var(--black-rgb), 0.3),
    0 6px 14px rgba(var(--black-rgb), 0.26),
    0 18px 36px rgba(var(--black-rgb), 0.22);
  transition: margin 0.2s ease, filter 0.15s ease;
}
.my-ticket:first-child { margin-top: 0; }
/* First-render fade-in stagger — only plays once on mount (index-based
   delay set inline via my.js), not on every re-render/expand. Shared by
   the ticket wallet and every other card/tile that fades in on load
   (budget, reminders, benefit tiles, MD rows, timeline). Pure transform +
   opacity so it's compositor-only — cheap even with many staggered items. */
/* animation-fill-mode: both keeps the "to" state applied after the animation
   ends, so an element whose resting opacity isn't 1 (e.g. a lost-raffle
   card at 0.5) settles there directly instead of finishing the animation at
   opacity:1 and then visibly snapping down once the animation stops
   controlling the property. */
.my-enter { animation: p-card-enter 0.4s ease both; --enter-opacity: 1; }
@keyframes p-card-enter {
  from { opacity: 0; transform: translateY(8px); }
  to   { opacity: var(--enter-opacity); transform: translateY(0); }
}
.my-lucky-card--lost.my-enter { --enter-opacity: 0.5; }

/* ── รอบที่ผ่านไปแล้ว (2026-08-21) ─────────────────────────────────────────
   Once a round's concert day is over, everything on the dashboard tied to
   that round fades to the same 0.4 this project already uses for a past item
   (.my-tl-item.is-past below; find.css's .find-assist-seat.is-past). Opacity
   only, no color of its own, so there is nothing to override per theme.

   Kept here rather than beside each component so the source order against
   .my-lucky-card--lost.my-enter above is unambiguous (equal specificity —
   later wins), and so the --enter-opacity rationale stays in one place.

   --enter-opacity is set ALONGSIDE opacity and is NOT optional on anything
   that went through applyEnter(): .my-enter's fade-in is `both`-filled, so
   its settled 100% keyframe (opacity: var(--enter-opacity)) beats a plain
   opacity rule at equal specificity — the same trap .my-gh-card--empty and
   .my-lucky-card--lost already document. refreshPastState() strips .my-enter
   from any node it reorders, so the plain opacity has to hold on its own too;
   hence both, not either. */
.my-ticket.is-past,
.my-lucky-card.is-past,
.my-guar-tile.is-past,
.my-gh-card.is-past { opacity: 0.4; --enter-opacity: 0.4; }
.my-chip.is-past { opacity: 0.4; }
/* A card already faded as a whole must not fade its chips a second time —
   0.4 x 0.4 compounds to an unreadable 0.16. */
.my-lucky-card.is-past .my-chip.is-past,
.my-guar-tile.is-past .my-chip.is-past { opacity: 1; }
@media (hover: hover) { .my-ticket:hover { filter: brightness(1.08); } }   /* no lift/gap, no dark shadow */
.my-ticket.is-open {
  margin-top: 12px;
  box-shadow: 0 8px 24px rgba(var(--shadow-rgb), calc(0.4 * var(--shadow-mult))), 0 0 0 1px var(--ticket-border, var(--border));
}
.my-ticket:first-child.is-open { margin-top: 0; }
.my-ticket.is-open + .my-ticket { margin-top: 12px; }
/* soft tier glow along the rail edge — rail stretches (flex default) to match
   .my-ticket__body's full height, padding included, so it always reaches the bottom */
.my-ticket__rail { width: 8px; flex-shrink: 0; box-shadow: 0 0 16px 2px var(--ticket-glow, transparent); }
.my-ticket__body { flex: 1; padding: 12px 14px; }
/* Stack order: first = backmost, last = frontmost (natural paint order).
   Back cards get bottom padding on the body (not the container) so the rail
   stretches to cover it too — front-most (last) card has none. */
.my-ticket:not(:last-child) .my-ticket__body { padding-bottom: 26px; }
.my-ticket.is-open .my-ticket__body { padding-bottom: 12px; }

/* Collapsed (peek) row */
.my-ticket__peek { display: flex; align-items: center; justify-content: space-between; position: relative; touch-action: pan-y; }
/* Long-press progress fill (2026-07-27, Feel Lab "long press" ref — 500ms
   hold with a visual progress fill) — a thin bar under the peek row that
   grows over the hold duration, tinted to the ticket's own tier color.
   Snaps back to 0 instantly (no transition) on release/cancel; only fills
   smoothly while actively pressed, via the .is-pressing override below. */
.my-ticket__peek::after {
  content: ''; position: absolute; left: 0; right: 0; bottom: -1px;
  height: 2px; width: 0%; border-radius: var(--radius-xs);
  background: var(--ticket-color, var(--accent-gold));
  pointer-events: none;
}
.my-ticket__peek.is-pressing::after { width: 100%; transition: width 0.5s linear; }
.my-ticket__facts { display: flex; align-items: flex-end; gap: 12px; }
.my-fact { display: flex; flex-direction: column; }
.my-fact__k { font-size: 9px; color: var(--text-secondary); }
.my-fact__v { font-size: 18px; font-weight: 700; line-height: 1; }
.my-ticket__status { display: flex; align-items: center; gap: 6px; font-size: 13px; }
.my-ticket__price {
  padding: 3px 8px; border-radius: var(--radius-md); margin-right: 2px;
  background: var(--ticket-glow, rgba(var(--surface-rgb),0.06));
  color: var(--ticket-color, var(--text-primary));
  font-size: 12px; font-weight: 700; white-space: nowrap;
}
.my-status-badge {
  width: 22px; height: 22px;
  display: flex; align-items: center; justify-content: center;
  border-radius: var(--radius-lg); font-size: 12px;
  background: rgba(var(--surface-rgb),0.08);
}

/* Expanded detail — hide the peek row to avoid duplicate zone/row/seat */
.my-ticket__detail { display: none; }
.my-ticket.is-open .my-ticket__peek { display: none; }
.my-ticket.is-open .my-ticket__detail { display: block; }
.my-ticket__detail-head { display: flex; justify-content: space-between; align-items: center; margin-bottom: 12px; }
.my-ticket__detail-badges { display: flex; align-items: center; gap: 6px; }
.my-ticket__num { font-size: 13px; color: var(--text-secondary); }
.my-badge {
  padding: 4px 10px; border-radius: var(--radius-lg); font-size: 11px; font-weight: 600;
}
.my-badge--verified { background: rgba(var(--status-green-rgb), 0.16); color: var(--success-text); }
.my-badge--pending  { background: rgba(250, 191, 89, 0.16); color: #FABF59; }
/* 2026-08-08 fix — same badge-pattern bug already fixed in market.css's
   .mk-badge--pending, missed here (measured 1.44:1 in light mode). */
:root[data-theme="light"] .my-badge--pending { color: #8A5800; }   /* 5.57 on its tint (was #9F6605, 4.43) */
.my-badge--unverified { background: rgba(var(--surface-rgb),0.08); color: var(--text-secondary); }
.my-badge--rejected { background: rgba(var(--status-red-rgb), 0.16); color: #FF8A8A; }   /* 6.03 on its tint (--danger-text measured 4.33) */
:root[data-theme="light"] .my-badge--rejected { color: #C01212; }   /* 4.80 (was --danger-text #D81616, 3.96-4.40) */
.my-badge--forsale { background: rgba(var(--accent-teal-rgb),0.16); color: var(--accent-teal-text); }

.my-ticket__big { display: flex; gap: 22px; margin-bottom: 12px; }
.my-big { display: flex; flex-direction: column; }
.my-big__k { font-size: 11px; color: var(--text-secondary); }
.my-big__v { font-size: 30px; font-weight: 700; line-height: 1.1; }

.my-ticket__meta { display: flex; justify-content: space-between; align-items: center; }
.my-meta__round { font-size: 13px; }
.my-meta__round b { font-size: 18px; }
.my-meta__date { font-size: 13px; }
.my-meta__date small { display: block; font-size: 11px; color: var(--text-secondary); }
.my-meta__price { font-size: 14px; font-weight: 700; color: var(--accent-gold-text); }

/* Lucky-draw hero cards (prominent random benefits) */
.my-lucky { display: flex; flex-direction: column; gap: 12px; }
.my-lucky__label { display: flex; align-items: center; gap: 8px; font-size: 13px; font-weight: 700; }
.my-lucky__badge {
  padding: 3px 9px; border-radius: var(--radius-pill);
  /* Fixed gradient, not theme-following (2026-08-08 fix) — same drift bug. */
  background: linear-gradient(120deg, var(--cta-grad-hi), var(--cta-grad-lo));
  color: var(--brand-gold-ink); font-size: 10px; font-weight: 800; letter-spacing: 0.5px;
}
.my-lucky__grid { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; }
.my-lucky-card {
  position: relative; overflow: hidden;
  display: flex; flex-direction: column; gap: 10px;
  padding: 16px 14px;
  border-radius: var(--radius-3xl);
  background:
    radial-gradient(circle at 100% 0%, rgba(var(--accent-teal-rgb),0.22), transparent 55%),
    linear-gradient(135deg, rgba(var(--accent-gold-rgb),0.30), var(--bg-secondary) 75%);
  border: 1px solid rgba(var(--accent-gold-rgb),0.55);
  box-shadow: 0 10px 26px rgba(var(--shadow-rgb), calc(0.35 * var(--shadow-mult)));
}
.my-lucky-card__spark {
  width: 34px; height: 34px; display: flex; align-items: center; justify-content: center;
  border-radius: var(--radius-lg); font-size: 17px;
  background: rgba(var(--surface-rgb),0.14);
}
.my-lucky-card__name { font-size: 15px; font-weight: 800; line-height: 1.25; }
.my-lucky-card__excl { font-size: 11px; color: rgba(var(--surface-rgb),0.7); margin-top: 2px; line-height: 1.4; }

/* Pending draw — normal (non-celebratory) mode: plain card, no gradient/glow */
.my-lucky-card--pending {
  background: var(--bg-secondary);
  border: 1px solid var(--border);
  box-shadow: none;
}
.my-lucky-card--pending .my-lucky-card__spark { background: rgba(var(--surface-rgb),0.06); }
.my-lucky-card--pending .my-lucky-card__name { font-weight: 600; color: var(--text-secondary); }
.my-lucky-card__waiting { font-size: 12px; color: var(--text-secondary); }

/* Lost raffle — dim the whole card, de-emphasized vs. a still-pending one */
.my-lucky-card--lost { opacity: 0.5; }

/* Seat chips (all seats grouped inside a single benefit box) */
.my-seat-chips { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 2px; }

/* Chips — seat identifiers (e.g. "R2 · A2 · B05"). Bordered pill instead of
   a flat gray fill, with the round prefix ("R2") picked out in teal so the
   round is scannable at a glance across a grid of chips. */
.my-chip {
  padding: 4px 9px; border-radius: var(--radius-md);
  background: rgba(var(--surface-rgb),0.05);
  border: 1px solid rgba(var(--surface-rgb),0.10);
  font-size: 10px; font-variant-numeric: tabular-nums;
  color: var(--text-secondary); white-space: nowrap;
}
.my-chip__round { color: var(--accent-teal-text); font-weight: 700; }
/* Awarded/won seat — gold glow border marks it as the special one among
   plain seat chips (previously identical styling to a losing/pending seat). */
.my-chip--win {
  align-self: flex-start;
  background: rgba(var(--accent-gold-rgb),0.14);
  border: 1px solid rgba(var(--accent-gold-rgb),0.55);
  box-shadow: 0 0 10px rgba(var(--accent-gold-rgb),0.25);
  color: var(--text-primary); font-weight: 700;
}
.my-chip--win .my-chip__round { color: var(--accent-gold-text); }

/* Guaranteed benefits — tile grid */
.my-guar-head { font-size: 12px; font-weight: 700; color: var(--text-secondary); margin: 2px 0; }
.my-guar-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; }
.my-guar-tile {
  display: flex; gap: 10px;
  padding: 12px;
  border-radius: var(--radius-xl);
  background: var(--bg-secondary);
  border: 1px solid var(--border);
}
.my-guar-tile__crest {
  width: 32px; height: 32px; flex-shrink: 0;
  display: flex; align-items: center; justify-content: center;
  border-radius: var(--radius-md); font-size: 16px; font-weight: 700;
  /* 2026-10-14: theme red + larger so the flat benefit icons read clearly
     (were 12px gray on a 24px tile, too faint after the emoji -> flat swap). */
  background: rgba(var(--accent-gold-rgb),0.16); color: var(--accent-gold-text);
}
.my-guar-tile__mid { min-width: 0; }
.my-guar-tile__name { font-size: 13px; font-weight: 600; line-height: 1.3; }
/* Name-to-chips gap (2026-07-10 fix) — the actual chip row here is
   `.my-seat-chips` (built by seatChips() in my.js), not `.my-guar-tile__chips`
   — that class was never applied to anything, so its margin-top never took
   effect and the chips sat right up against the name. Scoped to this tile
   only; `.my-seat-chips`'s own base margin-top elsewhere (e.g. Lucky cards)
   is untouched. */
.my-guar-tile__mid .my-seat-chips { margin-top: 6px; }

/* Fan Benefits (legacy list — kept for fallback) */
.my-benefits { display: flex; flex-direction: column; gap: 4px; }
.my-benefits__head { font-size: 11px; color: var(--text-secondary); margin-bottom: 4px; }
.my-benefit__chips { display: flex; flex-wrap: wrap; gap: 5px; margin-top: 4px; }
.my-benefit { display: flex; align-items: center; gap: 12px; padding: 8px 0; }
.my-benefit__icon {
  width: 26px; height: 26px; flex-shrink: 0;
  display: flex; align-items: center; justify-content: center;
  border-radius: var(--radius-md); font-size: 13px;
  background: rgba(var(--accent-gold-rgb),0.14);
}
.my-benefit__check {
  width: 20px; height: 20px; flex-shrink: 0;
  display: flex; align-items: center; justify-content: center;
  border-radius: var(--radius-sm); font-size: 11px;
  background: rgba(var(--status-green-rgb), 0.16); color: var(--success-text);
}
.my-benefit__mid { flex: 1; min-width: 0; }
.my-benefit__name { font-size: 14px; font-weight: 600; }
.my-benefit--all .my-benefit__name { flex: 1; font-weight: 500; }
.my-benefit__sub { font-size: 11px; color: var(--text-secondary); margin-top: 2px; }
.my-benefit__tag {
  flex-shrink: 0;
  padding: 4px 8px; border-radius: var(--radius-md);
  background: rgba(var(--surface-rgb),0.06);
  font-size: 11px; color: var(--text-secondary);
}

/* MD summary */
.my-md { display: flex; flex-direction: column; }
.my-md__row { display: flex; align-items: center; gap: 12px; padding: 8px 0; border-bottom: 1px solid var(--border); }
.my-md__thumb {
  width: 44px; height: 44px; flex-shrink: 0;
  border-radius: var(--radius-md); overflow: hidden;
  background: linear-gradient(135deg, var(--bg-elevated), var(--bg-secondary));
}
/* Theme-aware placeholder (2026-08-08 fix) — see .my-gv-card__thumb's
   identical comment below; this fixed dark gradient shows briefly before
   the real photo loads and read as out of place on an otherwise-light page. */
:root[data-theme="light"] .my-md__thumb { background: linear-gradient(135deg, var(--bg-elevated), var(--bg-secondary)); }
.my-md__thumb img { width: 100%; height: 100%; object-fit: cover; }
.my-md__thumb.is-clickable { cursor: pointer; }
.my-md__mid { flex: 1; min-width: 0; }
/* Single-line truncate, not 2-line clamp (2026-08-08 fix) — clamping to 2
   lines was still tall enough that a cart with most real product names
   (which run long — see below) dragged the whole /p/my page down badly
   once more than a few items were planned. Name is now purely identifying
   text (hence the lighter weight); the price carries the visual weight. */
.my-md__name {
  font-size: 12.5px; font-weight: 500;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.my-md__qty { font-size: 11px; color: var(--text-secondary); margin-top: 2px; }
/* Per-unit price appended here (qty > 1 only — see renderMdSummary in
   my.js) instead of its own line, so a row with real per-item pricing
   stays 2 lines total, same as a plain qty-only row. Redundant when
   qty === 1 (per-unit === row total, shown at right already), so that
   case is left as a bare qty line by the caller. */
.my-md__price { font-size: 13.5px; font-weight: 600; color: rgba(var(--accent-gold-rgb),0.75); }
/* Summary style (2026-08-08 fix) — was a flat "รวมสินค้า MD" label + price,
   read like just another row. Now states the count/qty explicitly ("รวม
   ทั้งหมด 2 รายการ · 3 ชิ้น") so it reads as a receipt-style total line,
   distinct from the item rows above it. */
.my-md__total { display: flex; justify-content: space-between; align-items: baseline; padding-top: 14px; }
.my-md__total-label { font-size: 12.5px; color: var(--text-secondary); }
.my-md__total-count { color: var(--text-primary); font-weight: 600; }
.my-md__total-val { font-size: 16px; font-weight: 700; color: var(--accent-gold-text); }

/* แพลนกลับบ้าน (Ride Pact) summary — card-per-round, same visual recipe as
   Ride Pact's own wallet (gohome.css .gh-wallet-card): teal top-border
   while planned, gold once matched, dashed gold CTA card when empty. */
.my-gh-list { display: flex; flex-direction: column; gap: 10px; }
.my-gh-card {
  display: flex; align-items: center; justify-content: space-between; gap: 10px;
  border-radius: var(--radius-2xl); border: 1px solid var(--border); background: var(--bg-secondary);
  padding: 14px; color: var(--text-primary); text-decoration: none;
}
.my-gh-card--planned { border-top: 2px solid var(--accent-teal); }
.my-gh-card--planned.is-success { border-top-color: var(--accent-gold); }
/* De-emphasized (2026-07-27 ask) — a not-yet-planned round shouldn't compete
   visually with one that's actually planned. --enter-opacity (not opacity
   directly) is required — .my-enter's fade-in animation is `both`-filled, so
   its own 100% keyframe (opacity: var(--enter-opacity), default 1) would
   otherwise win over a plain opacity rule once the animation settles; same
   pattern as .my-lucky-card--lost above. */
.my-gh-card--empty { border-style: dashed; opacity: 0.55; --enter-opacity: 0.55; transition: opacity 0.15s ease, border-color 0.15s ease; }
@media (hover: hover) { .my-gh-card--empty:hover { opacity: 0.85; border-color: var(--accent-gold); } }
/* Round/date — small caption (2026-07-27 ask, was the dominant line);
   destination below is now the emphasized element instead. */
.my-gh-round { font-size: 11px; font-weight: 600; color: var(--text-secondary); }
.my-gh-dest { display: flex; align-items: center; gap: 6px; font-size: 14px; font-weight: 600; color: var(--text-primary); margin-top: 3px; } /* matches .my-benefit__name (2026-07-27 ask) */
.my-gh-cta { color: var(--text-secondary); font-size: 13px; font-weight: 600; flex-shrink: 0; }
.my-gh-success { font-size: 18px; flex-shrink: 0; }

/* Giveaway ของฉัน (Fan Forge) summary — card-per-entry, echoing Nova
   Craft's own .nt-card (rounded photo thumb + title + status) scaled down
   for a dashboard list instead of a browse grid. */
.my-gv-list { display: flex; flex-direction: column; gap: 10px; margin-bottom: 12px; }
.my-gv-card {
  display: flex; align-items: center; gap: 12px;
  border-radius: var(--radius-2xl); border: 1px solid var(--border); background: var(--bg-secondary);
  padding: 10px; color: var(--text-primary); text-decoration: none;
  transition: border-color 0.15s ease;
}
@media (hover: hover) { .my-gv-card--approved:hover { border-color: var(--accent-teal); } }
.my-gv-card__thumb {
  width: 52px; height: 52px; flex-shrink: 0;
  border-radius: var(--radius-lg); overflow: hidden;
  background: linear-gradient(135deg, var(--bg-elevated), var(--bg-secondary));
}
/* Theme-aware placeholder (2026-08-08 fix) — shows briefly before the real
   giveaway photo loads; a fixed dark box read as out of place on an
   otherwise-light page. */
:root[data-theme="light"] .my-gv-card__thumb { background: linear-gradient(135deg, var(--bg-elevated), var(--bg-secondary)); }
.my-gv-card__thumb img { width: 100%; height: 100%; object-fit: cover; }
.my-gv-card__mid { flex: 1; min-width: 0; }
.my-gv-card__title { font-size: 14px; font-weight: 600; }
.my-gv-card__reason { font-size: 11px; color: var(--danger-text); margin-top: 2px; }

/* Reminders — "เรื่องสำคัญ": high-visibility alert so people stop and read */
.my-card.my-reminders {
  background: linear-gradient(135deg, rgba(var(--like-red-rgb), 0.14), var(--bg-secondary));
  border: 1px solid rgba(var(--like-red-rgb), 0.5);
  border-left: 4px solid var(--like-red);
  box-shadow: 0 6px 20px rgba(var(--like-red-rgb), 0.12);
}
.my-reminders__title { display: flex; align-items: center; gap: 8px; font-size: 15px; font-weight: 800; margin-bottom: 10px; color: #ff8a9b; }
/* 2026-08-08 fix — #ff8a9b measured ~1.95:1 in light mode (this card's
   background blends into the theme-aware --bg-secondary). Same hue,
   darkened to ~3.3:1. */
:root[data-theme="light"] .my-reminders__title { color: #E70022; }
.my-reminders__title span:first-child { font-size: 18px; }
.my-reminder { display: flex; gap: 10px; font-size: 12.5px; color: var(--text-primary); padding: 7px 0; line-height: 1.55; }
.my-reminder__dot { width: 7px; height: 7px; border-radius: var(--radius-full); background: var(--like-red); margin-top: 6px; flex-shrink: 0; box-shadow: 0 0 8px rgba(var(--like-red-rgb), 0.8); }
:root[data-theme="light"] .my-reminder__dot { background: var(--status-like); }
.my-reminder__link { color: var(--accent-gold-text); text-decoration: underline; cursor: pointer; }
.my-reminder__date { color: var(--accent-gold-text); font-weight: 700; }

/* Health Status Check-In banner (§3.1) — 3 states, all built on .my-card.
   Only the "sick" answered state (covid/flu/other) reuses the reminders
   card's alert-red language; healthy uses teal, not_tested uses a neutral
   grey so it never reads as "confirmed healthy" (Decision #17). */
.my-health-wrap { display: contents; }
/* .my-health-wrap is display:contents, so its 2 children (the card + the
   consent notice below it) fall directly into the parent flex column's own
   gap: 16px (.my-content / .my-group--left) — the same spacing used between
   unrelated dashboard sections. Pulled back in here (2026-07-26 fix) so the
   notice reads as attached to the banner right above it, not a separate
   section of its own. */
.my-health-wrap .health-notice { margin-top: -8px; text-align: left; }
.my-health__title { font-size: 13px; font-weight: 700; margin-bottom: 10px; }
.my-health__pills { display: flex; flex-wrap: wrap; gap: 8px; }
.my-health__pill {
  display: flex; align-items: center; gap: 6px;
  padding: 9px 14px;
  background: rgba(var(--surface-rgb),0.05);
  border: 1px solid rgba(var(--surface-rgb),0.15);
  border-radius: var(--radius-pill);
  color: var(--text-primary);
  font: 600 12.5px var(--font-thai);
  cursor: pointer;
  transition: border-color 0.12s ease, background 0.12s ease;
}
@media (hover: hover) { .my-health__pill:hover { border-color: var(--accent-gold); background: rgba(var(--accent-gold-rgb),0.1); } }
.my-health__pill:disabled { opacity: 0.5; cursor: default; }
.my-card.my-health--unanswered { border-left: 4px solid var(--accent-gold); }

.my-health__row { display: flex; align-items: center; gap: 10px; }
.my-health__icon { font-size: 20px; }
.my-health__label { flex: 1; font-size: 13.5px; font-weight: 700; }
.my-health__edit {
  background: none; border: none; padding: 4px 0;
  color: var(--accent-gold-text);
  font: 600 12px var(--font-thai);
  cursor: pointer;
}
/* "คำตอบเมื่อ: …" under the answered card — same muted 10px treatment as
   .tk-reg-dates__line on /p/tickets (2026-08-24 ask). */
.my-health__answered-at { margin-top: 6px; font-size: 10px; color: var(--text-secondary); opacity: 0.75; }
/* .my-health--not-tested retired 2026-08-24 — a skip now renders as the
   unanswered state (4 pills) until a real status is picked; see
   renderHealthBanner() in my.js. */
.my-card.my-health--answered.is-healthy { border-left: 4px solid var(--accent-teal); }
.my-card.my-health--answered.is-healthy .my-health__label { color: var(--accent-teal); }
.my-card.my-health--answered.is-sick {
  background: linear-gradient(135deg, rgba(var(--like-red-rgb), 0.14), var(--bg-secondary));
  border: 1px solid rgba(var(--like-red-rgb), 0.5);
  border-left: 4px solid var(--like-red);
}
.my-card.my-health--answered.is-sick .my-health__label { color: #ff8a9b; }
/* 2026-08-08 fix — same #ff8a9b bug as .my-reminders__title above. */
:root[data-theme="light"] .my-card.my-health--answered.is-sick .my-health__label { color: #E70022; }

/* "มีอะไรให้ช่วยดูแลไหม?" card (2026-08-17) — sits right after "บัตรของฉัน".
   No colored border-left like Health's states on purpose — this is a light
   peer-to-peer ask, not a status alert. */
.my-card.my-assist { margin-top: 12px; }
.my-assist__prompt-row { display: flex; align-items: center; gap: 10px; margin-bottom: 12px; }
.my-assist__prompt-icon { font-size: 20px; }
.my-assist__prompt-label { flex: 1; font-size: 15px; font-weight: 700; }
.my-assist__btn-row { display: flex; gap: 8px; }
.my-assist__btn {
  flex: 1;
  padding: 10px;
  background: rgba(var(--surface-rgb), 0.05);
  border: 1px solid rgba(var(--surface-rgb), 0.15);
  border-radius: var(--radius-lg);
  color: var(--text-primary);
  font: 600 13px var(--font-thai);
  cursor: pointer;
  transition: border-color 0.12s ease, background 0.12s ease;
}
@media (hover: hover) { .my-assist__btn:hover { border-color: var(--accent-gold); } }
.my-assist__btn:disabled { opacity: 0.5; cursor: default; }
/* Toned down from a solid accent-gold border/fill (2026-08-18 ask) — read as
   too heavy next to plain .my-card siblings like Budget/Timeline. */
.my-assist__btn--yes { border-color: rgba(var(--accent-gold-rgb), 0.5); background: rgba(var(--accent-gold-rgb), 0.06); color: var(--accent-gold-text); }
.my-assist__row { display: flex; align-items: center; gap: 10px; }
.my-assist__empty-label { flex: 1; font-size: 13.5px; color: var(--text-secondary); }
/* Faint-until-hover edit link (2026-08-17 ask) — the "แก้ไข" affordance stays
   quiet so the ไม่มี/answered states read as settled, not as a pending task. */
.my-assist__edit {
  flex-shrink: 0;
  background: none; border: none; padding: 4px 0;
  color: var(--accent-gold-text);
  font: 600 13px var(--font-thai);
  cursor: pointer;
  opacity: 0.45;
  transition: opacity 0.12s ease;
}
.my-assist__edit:focus-visible { opacity: 1; }
@media (hover: hover) { .my-assist__edit:hover { opacity: 1; } }
.my-assist__chips { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 10px; }
.my-assist__chip {
  display: flex; flex-direction: column; gap: 3px;
  padding: 8px 12px;
  background: rgba(var(--accent-gold-rgb), 0.06);
  border: 1px solid rgba(var(--accent-gold-rgb), 0.18);
  border-radius: var(--radius-xl);
}
.my-assist__chip-top { display: flex; align-items: center; gap: 6px; color: var(--accent-gold-text); font: 600 13px var(--font-thai); }
.my-assist__chip-severity { color: var(--text-secondary); font: 400 12px var(--font-thai); }
.my-assist--answered .health-notice { margin-top: 10px; text-align: left; }

/* Concert Timeline — tabbed per day */
.my-tl-tabs { display: flex; gap: 8px; margin-bottom: 16px; }
.my-tl-tab {
  flex: 1; display: flex; flex-direction: column; gap: 3px; align-items: center; justify-content: center;
  padding: 10px 6px; border-radius: var(--radius-xl);
  background: rgba(var(--surface-rgb),0.04);
  border: 1px solid var(--border);
  cursor: pointer; color: var(--text-secondary);
  font-family: var(--font-thai);
  text-align: center; line-height: 1.25;
  transition: background 0.15s ease, border-color 0.15s ease, color 0.15s ease;
}
.my-tl-tab.is-active {
  background: rgba(var(--accent-gold-rgb),0.16);
  border-color: rgba(var(--accent-gold-rgb),0.7);
  color: var(--text-primary);
}
.my-tl-tab__day { font-size: 13px; font-weight: 700; line-height: 1.2; }
.my-tl-tab__date { font-size: 10px; line-height: 1.2; }
.my-tl-date { font-size: 12px; color: var(--accent-gold-text); font-weight: 600; margin-bottom: 12px; }

/* Concert Timeline */
.my-timeline { display: flex; flex-direction: column; }
.my-tl-round { display: flex; align-items: center; gap: 14px; padding: 14px 0 10px; font-size: 13px; font-weight: 600; }
.my-tl-round:first-child { padding-top: 0; }
.my-tl-round__dot { width: 8px; height: 8px; border-radius: var(--radius-full); background: var(--accent-gold); flex-shrink: 0; }
:root[data-theme="light"] .my-tl-round__dot { background: var(--brand-gold); }
.my-tl-item { display: flex; gap: 12px; }
.my-tl-rail { position: relative; width: 12px; flex-shrink: 0; }
.my-tl-rail .my-tl-line { position: absolute; left: 5px; top: 3px; bottom: -8px; width: 2px; background: var(--border); }
.my-tl-item.is-last .my-tl-line { display: none; }
.my-tl-rail .my-tl-dot { position: absolute; left: 1px; top: 3px; width: 10px; height: 10px; border-radius: var(--radius-full); background: var(--accent-teal); }
:root[data-theme="light"] .my-tl-rail .my-tl-dot { background: var(--brand-teal); }
.my-tl-body { flex: 1; padding-bottom: 14px; }
/* Time vs label contrast (2026-08-12): time carries the accent color/weight,
   label stays regular-weight white — differentiates the two without resizing
   either (kept at the original 14px/13px). */
.my-tl-time { font-size: 14px; font-weight: 700; color: var(--accent-teal-text); }
.my-tl-label { font-size: 13px; font-weight: 400; color: var(--text-primary); margin-top: 2px; }
/* Concert-start (last item): bold, vibrant electric-blue label matching its
   own dot (glow added for extra pop against the dark bg) — every other item
   stays teal. #0170FE (2026-07-08) read as too muted/low-contrast against
   --bg-primary; swapped for a brighter cyan-blue (2026-07-08 follow-up). */
.my-tl-item.is-last .my-tl-dot { background: #FF1E1E; box-shadow: 0 0 8px rgba(255, 30, 30, 0.7); }
.my-tl-item.is-last .my-tl-label { color: #FF1E1E; font-weight: 700; }
/* This row's time stays plain white, not teal — the label already carries
   the special blue treatment above, so the time doesn't need the new accent
   color too (2026-08-12 ask). */
.my-tl-item.is-last .my-tl-time { color: var(--text-primary); }
/* Text-only override (2026-08-08 fix, found in a codebase-wide literal-hex
   sweep, not by the earlier systematic passes — this was another one-off
   dark-mode-tuned color nobody had tokenized) — #00D4FF measured 1.54:1 in
   light mode. Same hue, darkened to ~3.3:1. The dot itself stays #00D4FF in
   both themes (a small color swatch, not read as text). */
:root[data-theme="light"] .my-tl-item.is-last .my-tl-label { color: #A61C19; }
/* Time already passed — faded like the Concert Guide modal's schedule tab
   (.guide-tl-past in style.css), not recolored, so it still reads in both
   themes and doesn't fight the is-last teal/blue treatment above. */
.my-tl-item.is-past { opacity: 0.4; }

/* ── Floating jump-nav (2026-07-27 ask, revised) — bottom-LEFT shortcut to
   jump between dashboard sections (both mobile and desktop). Left side keeps
   it clear of the shared "back to top" button (.ppw-back-to-top, tokens.css
   — right:16px). Menu uses the same glass/blur elevation as the navbar's own
   dropdowns (nav.css .ppw-nav__dropdown) so it doesn't read as a flat gold
   box against the space background. */
.my-jump-nav { position: fixed; left: calc(16px + var(--safe-left)); bottom: calc(20px + var(--safe-bottom) + var(--inapp-bottom)); z-index: 900; }
.my-jump-nav__btn {
  width: 44px; height: 44px; border-radius: var(--radius-full);
  display: flex; align-items: center; justify-content: center;
  /* Fixed gradient, not theme-following (2026-08-08 fix) — same drift bug. */
  background: linear-gradient(135deg, var(--cta-grad-hi), var(--cta-grad-lo));
  border: none; box-shadow: 0 4px 14px rgba(var(--accent-gold-rgb),0.45), inset 0 1px 0 rgba(255, 255, 255, .18);
  color: var(--brand-gold-ink); font-size: 18px; cursor: pointer;
  opacity: 0.6; transition: opacity 0.15s ease, transform 0.15s ease;
}
@media (hover: hover) { .my-jump-nav__btn:hover { opacity: 1; } }
.my-jump-nav__btn:active { transform: scale(0.94); }
/* Stays fully visible while the menu itself is open (2026-07-27 ask —
   fading is only meant to de-emphasize the idle button). */
.my-jump-nav.is-open .my-jump-nav__btn { opacity: 1; }
.my-jump-nav__menu {
  position: absolute; left: 0; bottom: 54px;
  /* Shrink-to-fit content (2026-07-27 fix) — a flat min-width left a lot of
     dead space to the right of short labels like "Timeline"; max-width caps
     it so a very long label still wraps instead of overflowing the screen. */
  width: max-content; max-width: min(240px, calc(100vw - 32px));
  border: 1px solid rgba(var(--surface-rgb),0.14);
  border-radius: var(--radius-2xl);
  padding: 6px;
  /* Hardcoded per-theme, not a token (2026-08-07 fix) — same reason as
     nav.css's .ppw-nav__dropdown glass-fill: CSS vars can't carry their own
     alpha, and this was never given the same light-mode branch, so it
     stayed a dark glass pill even in light mode. */
  background: rgba(17, 17, 17, 0.72);
  backdrop-filter: blur(20px) saturate(1.3);
  -webkit-backdrop-filter: blur(20px) saturate(1.3);
  box-shadow: 0 12px 28px rgba(var(--shadow-rgb), calc(0.45 * var(--shadow-mult)));
  display: flex; flex-direction: column; gap: 2px;
  opacity: 0; transform: translateY(6px) scale(0.98); pointer-events: none;
  transition: opacity 0.15s ease, transform 0.15s var(--ease-spring, ease);
}
.my-jump-nav.is-open .my-jump-nav__menu { opacity: 1; transform: translateY(0) scale(1); pointer-events: auto; }
:root[data-theme="light"] .my-jump-nav__menu { background: rgba(219, 212, 233, 0.85); }
:root[data-theme="dark"] .my-jump-nav__menu { background: rgba(17, 17, 17, 0.72); }
.my-jump-nav__item {
  display: flex; align-items: center; gap: 8px;
  padding: 9px 10px; border-radius: var(--radius-md);
  background: none; border: none; color: var(--text-primary);
  font-family: inherit; font-size: 13px; text-align: left; cursor: pointer;
}
.my-jump-nav__item:active { background: rgba(var(--surface-rgb),0.08); }
@media (hover: hover) { .my-jump-nav__item:hover { background: rgba(var(--surface-rgb),0.08); } }
/* Locked item (2026-08-11 ask) — section hidden because the member has 0
   tickets. Stays in the menu (not removed) but faded and non-interactive
   beyond the click handler itself, which shows a toast instead of scrolling. */
.my-jump-nav__item--locked { opacity: 0.45; cursor: not-allowed; }
.my-jump-nav__item--locked:active { background: none; }
@media (hover: hover) { .my-jump-nav__item--locked:hover { background: none; } }

/* Top-of-dashboard warning banner shown when the member has 0 tickets
   (2026-08-11 ask) — replaces the old hard gate that hid the whole page. */
.my-noticket-banner {
  display: flex; align-items: center; gap: 8px;
  padding: 12px 14px; border-radius: var(--radius-2xl);
  background: rgba(var(--accent-gold-rgb),0.12);
  border: 1px solid rgba(var(--accent-gold-rgb),0.35);
  font-size: 13px; line-height: 1.5; color: var(--text-primary);
}
/* line-height:1 (2026-08-11 fix) — without it the icon's own line box
   inherited the banner's 1.5 line-height at font-size:16px (=24px), floating
   the 16px icon inside a taller invisible box and reading as "puffy" next to
   the tighter text; align-items:center above centers it against the text
   block once that padding is gone, instead of pinning it to the top. */
.my-noticket-banner__icon { flex: none; font-size: 16px; line-height: 1; display: inline-flex; }
.my-noticket-banner__link { color: var(--accent-gold-text); font-weight: 700; text-decoration: underline; }
@media (min-width: 768px) {
  /* Spans both bento columns instead of sitting only in the grid's first
     column (its default auto-placement slot). */
  .my-noticket-banner { grid-column: 1 / -1; }
}
.my-tl-sub { font-size: 11px; color: var(--text-secondary); opacity: 0.8; margin-top: 2px; line-height: 1.4; }

/* ── "Add to home screen" banner (2026-08-14 ask) — sits as the very last
   element before the legal footer (my.js's render(), after content.append).
   Design 2 of 5 mockups presented to the user: compact horizontal banner,
   same card language as the rest of the dashboard (bg-secondary card on
   bg-primary page). */
.a2hs-banner {
  position: relative;
  display: flex;
  flex-direction: column;
  gap: 12px;
  padding: 14px 16px;
  background: var(--bg-secondary);
  border: 1px solid var(--border);
  border-radius: var(--radius-2xl);
}
/* Solid-by-default (not just on :hover) + a visible ring (2026-08-14 ask:
   "ต้องมีปุ่มปิดมุมขวาชัดเจน") — the old transparent-until-hover treatment
   read as invisible chrome rather than an actual close control, especially
   on touch where there's no hover state to reveal it at all.
   Floating again — position:absolute, top-right corner (2026-08-14, later
   same day: reverted back from the row's in-flow last item). The in-flow
   placement was itself a fix for this exact button overlapping the CTA once
   the banner narrowed to match the Concert Timeline column's width; now that
   it floats again, .a2hs-row below reserves clearance via padding-right
   instead, so the CTA still can't slide underneath it.
   Negative top/right, not a positive inset (2026-08-14, same-day follow-up:
   "ขอแบบลอยเลย ไม่ใช่เอาไว้ชิดขอบ") — a positive offset sits the button
   flush INSIDE the card's own corner, still touching both edges; a true
   "floating" badge protrudes OUTSIDE the card boundary, overlapping the
   border on both sides, with its own shadow lifting it off the surface. */
.a2hs-close {
  position: absolute;
  top: -10px; right: -10px;
  width: 24px; height: 24px;
  box-shadow: 0 4px 10px rgba(var(--shadow-rgb), calc(0.35 * var(--shadow-mult)));
  display: flex; align-items: center; justify-content: center;
  border: 1px solid var(--border);
  border-radius: var(--radius-full);
  background: rgba(var(--surface-rgb),0.08);
  color: var(--text-primary);
  cursor: pointer;
}
@media (hover: hover) { .a2hs-close:hover { background: rgba(var(--surface-rgb),0.16); } }
/* display:block on the icon SVGs (2026-08-14 fix) — an inline <svg> carries
   the same baseline/half-leading offset as inline text, so it sat off-center
   inside its flex-centered wrapper (circle/box) instead of landing dead
   center; block removes that line-box influence entirely. */
.a2hs-close svg { display: block; width: 12px; height: 12px; }
/* padding-right recalculated + align-items reverted to center (2026-08-14,
   same-day follow-up: "ทำไมไม่ชิดขวา... ชั้นต้องการ center middle").
   The 28px buffer above was sized back when .a2hs-close was position:10px
   inset INSIDE the card, genuinely overlapping the row's own vertical
   space. Once it moved to a negative top/right float (protruding mostly
   ABOVE the card border), its real footprint is y:[-10px, 14px] relative
   to the banner's top edge — which just barely touches the row's own start
   (banner's 14px top padding) and no longer meaningfully overlaps it.
   Carrying the old 28px buffer forward pushed the CTA needlessly far from
   the card's right edge, reading as "not flush right" — a stale value from
   the button's PREVIOUS position, not a real clearance need for its
   current one. A small 6px buffer is enough real safety margin; align-items
   goes back to center (the 2-line-title "sinking" concern from the
   previous fix is real but secondary to this — center is what was asked
   for here). */
.a2hs-row { display: flex; align-items: center; gap: 12px; padding-right: 6px; }
.a2hs-icon {
  flex-shrink: 0;
  width: 38px; height: 38px;
  border-radius: var(--radius-lg);
  display: flex; align-items: center; justify-content: center;
  background: rgba(var(--accent-gold-rgb),0.14);
  color: var(--accent-gold-text);
  font-size: 19px;
}
.a2hs-icon svg { display: block; }
.a2hs-text { flex: 1; min-width: 0; }
.a2hs-title { font-size: 13px; font-weight: 700; color: var(--text-primary); }
.a2hs-sub { font-size: 11px; color: var(--text-secondary); margin-top: 2px; }
.a2hs-cta {
  flex-shrink: 0;
  display: inline-flex; align-items: center; gap: 5px;
  padding: 8px 14px;
  border: none;
  border-radius: var(--radius-md);
  background: var(--accent-gold);
  color: var(--gold-btn-text);
  font: 700 12px var(--font-thai);
  cursor: pointer;
  white-space: nowrap;
}
@media (hover: hover) { .a2hs-cta:hover { filter: brightness(1.05); } }
.a2hs-cta__icon { display: inline-flex; width: 14px; height: 14px; }
.a2hs-cta__icon svg { display: block; }
.a2hs-steps {
  display: flex; flex-direction: column; gap: 9px;
  border-top: 1px solid var(--border);
  padding-top: 12px;
}
.a2hs-step { display: flex; align-items: flex-start; gap: 9px; }
.a2hs-step__num {
  flex-shrink: 0;
  width: 18px; height: 18px;
  border-radius: var(--radius-full);
  background: rgba(var(--surface-rgb),0.08);
  color: var(--text-secondary);
  font-size: 10px; font-weight: 700;
  display: flex; align-items: center; justify-content: center;
}
.a2hs-step__text { font-size: 12px; color: var(--text-secondary); line-height: 1.5; }

/* 3D seat view entry links (impact-3d-link.js): dimmed until the phase opens. */
.impact3d-locked { opacity: 0.45; pointer-events: none; }
.my-ticket__3d {
  display: flex; align-items: center; justify-content: center; gap: 6px;
  margin-top: 10px;
  padding: 7px 12px;
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  color: var(--text-primary);
  font: 600 12px var(--font-thai);
  text-decoration: none;
}
/* Seat map + 3D side by side. */
.my-seatmap-row { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 8px; }
/* Inside a ticket a dimmed link still swallows the tap, or the card would collapse. */
.my-ticket__3d.impact3d-locked { pointer-events: auto; cursor: not-allowed; }
