/* ============================================================
   SCE App — light theme
   Matches the printed daily program: white card stock, colored
   category bands, Red/Blue seating cards.

   Constraints that drove this:
   1. Read on a phone by guests aged 50-70, often at arm's length.
      Nothing below 17px. Generous tap targets.
   2. NO external font CDN — a Google Fonts link needs its own
      whitelist entry and would fall back to system faces at sea.
      System stack only.
   3. Seating color OVERRIDES category color. If you're in Red
      seating, finding your show beats knowing its genre.
   ============================================================ */

:root {
  --paper:      #FFFFFF;
  --paper-2:    #F6F5F2;   /* page ground, slightly warm */
  --rule:       #DFDCD6;   /* hairlines */
  --ink:        #14161A;   /* primary text */
  --ink-2:      #5C6470;   /* secondary text */
  --ink-3:      #8A929E;   /* tertiary / disabled */

  /* Category colors — from the printed program */
  --enjoy:       #07DA00;
  --in-concert:  #000000;
  --learn:       #F3895B;
  --information: #E0E0E0;
  --red-card:    #FF3E3E;
  --blue-card:   #2900F3;

  /* Card washes. Deliberately faint: enough to tell cards apart while
     scanning, never enough to fight the text on top. */
  --title-ink:     #000000;   /* event title on a tinted card */
  --postponed-ink: #A1450F;   /* "Postponed" badge — dark enough to read */
  --meta-ink:      #4A515C;   /* venue / deck line on a tinted card */
  --tint-enjoy:    #07DA0016;
  --tint-concert:  #11111110;
  --tint-learn:    #F3895B22;
  --tint-info:     #E0E0E038;
  --tint-red:      #FF3E3E1C;
  --tint-blue:     #2900F312;

  --accent:     #2900F3;   /* interactive elements */
  --accent-ink: #2900F3;   /* the same blue, used AS text -- see dark mode */
  --on-accent:  #FFFFFF;   /* text sitting on an accent fill */

  --sans: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
  --rail: 66px;
  --tap: 48px;
}

* { box-sizing: border-box; }
html { -webkit-text-size-adjust: 100%; }

body {
  margin: 0;
  background: var(--paper-2);
  color: var(--ink);
  font-family: var(--sans);
  font-size: 17px;
  line-height: 1.45;
  padding-bottom: calc(76px + env(safe-area-inset-bottom));
}

a { color: inherit; text-decoration: none; }

:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

/* ---------- Header ---------- */

.topbar {
  position: sticky; top: 0; z-index: 20;
  background: var(--paper);
  border-bottom: 1px solid var(--rule);
  padding: calc(12px + env(safe-area-inset-top)) 16px 12px;
}
.topbar h1 { margin: 0; font-size: 19px; font-weight: 650; letter-spacing: -0.01em; }
.topbar .sub {
  margin-top: 2px; font-size: 12.5px; color: var(--ink-2);
  text-transform: uppercase; letter-spacing: 0.09em;
}

/* ---------- Day tabs ---------- */

.days {
  display: flex; gap: 8px; overflow-x: auto;
  padding: 12px 16px; scrollbar-width: none;
  background: var(--paper);
  border-bottom: 1px solid var(--rule);
}
.days::-webkit-scrollbar { display: none; }

.day {
  flex: 0 0 auto; min-width: 58px; min-height: var(--tap);
  padding: 7px 13px;
  border: 1px solid var(--rule); border-radius: 10px;
  text-align: center; background: var(--paper);
}
.day .n { display: block; font-size: 17px; font-weight: 650; }
.day .d { display: block; font-size: 12px; color: var(--ink-2); }
.day[aria-current="true"] {
  background: var(--ink); border-color: var(--ink); color: var(--paper);
}
.day[aria-current="true"] .d { color: rgba(255,255,255,.7); }

/* ---------- Theme banner ---------- */

.theme {
  padding: 11px 16px;
  background: var(--paper);
  border-bottom: 1px solid var(--rule);
  font-size: 14.5px; color: var(--ink-2);
}
.theme strong {
  color: var(--ink); font-weight: 650;
  text-transform: uppercase; letter-spacing: 0.07em;
  font-size: 12.5px; margin-right: 7px;
}

/* ---------- Time rail ---------- */

.rail { position: relative; padding: 6px 16px 32px; }

.rail::before {
  content: ""; position: absolute;
  left: calc(16px + var(--rail) - 13px);
  top: 0; bottom: 32px; width: 1px; background: var(--rule);
}

.slot {
  position: relative; display: grid;
  grid-template-columns: var(--rail) 1fr;
  gap: 12px; padding: 9px 0; min-height: var(--tap);
}

.slot .time {
  text-align: right; padding-right: 14px;
  font-size: 19px; font-weight: 700;
  font-variant-numeric: tabular-nums; letter-spacing: -0.02em;
  color: var(--ink);
}
.slot .time .ampm {
  display: block; font-size: 10.5px; font-weight: 600;
  letter-spacing: 0.12em; color: var(--ink-3);
}

.slot::after {
  content: ""; position: absolute;
  left: calc(var(--rail) - 17px); top: 19px;
  width: 9px; height: 9px; border-radius: 50%;
  background: var(--paper-2); border: 2px solid var(--rule);
}

/* ---------- The card ----------
   The colored band down the left is the strong signal, and the whole
   card carries a wash of the same colour so a category reads at arm's
   length -- like the printed program, but pale enough that titles stay
   black-on-light. Full-saturation cards were tried and rejected: In
   Concert is black, which vanishes on a dark page, and a red "RED
   SEATING" pill disappears on a red card. */

.slot .card {
  background: var(--paper);
  border: 1px solid var(--rule);
  border-left: 5px solid var(--information);
  border-radius: 8px;
  padding: 11px 13px;
}

.slot[data-cat="enjoy"]       .card { border-left-color: var(--enjoy);
                                      background: var(--tint-enjoy); }
.slot[data-cat="in_concert"]  .card { border-left-color: var(--in-concert);
                                      background: var(--tint-concert); }
.slot[data-cat="learn"]       .card { border-left-color: var(--learn);
                                      background: var(--tint-learn); }
.slot[data-cat="information"] .card { border-left-color: var(--information);
                                      background: var(--tint-info); }

/* Seating overrides category — this is the point of the rotation */
.slot[data-seat="red"]  .card { border-left-color: var(--red-card);
                                background: var(--tint-red); }
.slot[data-seat="blue"] .card { border-left-color: var(--blue-card);
                                background: var(--tint-blue); }

/* Pure black on a tinted card, rather than the app's near-black: the
   wash lifts the background a little, and on a phone in daylight the
   extra step is worth having. Dark mode keeps the off-white. */
.slot .title { font-size: 17.5px; font-weight: 620; letter-spacing: -0.01em;
               color: var(--title-ink); }
.slot .performers { margin-top: 2px; font-size: 14.5px; color: var(--ink); font-weight: 500; }
/* One shade darker than body secondary text: the tint lifts the card
   background, which cost the venue line some of its contrast. 6.9:1 on
   the strongest tint, against 5.2:1 before. */
.slot .meta { margin-top: 2px; font-size: 14px; color: var(--meta-ink); }

.slot[data-cancelled="true"] .title {
  text-decoration: line-through; color: var(--ink-3);
}

/* ---------- Seating badge ---------- */

.seat {
  display: inline-block; margin-top: 7px;
  padding: 2px 9px; border-radius: 999px;
  font-size: 11px; font-weight: 700;
  text-transform: uppercase; letter-spacing: 0.09em;
  color: #fff;
}
.seat[data-seat="red"]  { background: var(--red-card); }
.seat[data-seat="blue"] { background: var(--blue-card); }

.tag {
  display: inline-block; margin-top: 7px; margin-right: 6px;
  padding: 2px 9px; border: 1px solid var(--rule); border-radius: 999px;
  font-size: 11px; text-transform: uppercase;
  letter-spacing: 0.08em; color: var(--ink-2);
}

/* ---------- Legend ---------- */

.legend {
  display: flex; flex-wrap: wrap; gap: 6px 14px;
  padding: 11px 16px;
  background: var(--paper); border-bottom: 1px solid var(--rule);
  font-size: 12.5px; color: var(--ink-2);
}
.legend span { display: flex; align-items: center; gap: 6px; }
.legend i {
  width: 11px; height: 11px; border-radius: 3px;
  border: 1px solid rgba(0,0,0,.14);
}

/* ---------- Bottom nav ---------- */

.nav {
  position: fixed; left: 0; right: 0; bottom: 0; z-index: 30;
  display: grid; grid-template-columns: repeat(3, 1fr);
  background: var(--paper); border-top: 1px solid var(--rule);
  padding-bottom: env(safe-area-inset-bottom);
}
.nav a {
  min-height: 60px; display: flex; flex-direction: column;
  align-items: center; justify-content: center; gap: 3px;
  font-size: 12px; letter-spacing: 0.03em; color: var(--ink-2);
}
.nav a[aria-current="page"] { color: var(--accent-ink); font-weight: 600; }
.nav .glyph { font-size: 19px; line-height: 1; }

/* ---------- Page furniture ---------- */

.wrap { padding: 20px 16px 32px; }

.empty {
  margin: 40px 16px; padding: 26px 22px;
  border: 1px dashed var(--rule); border-radius: 12px;
  text-align: center; color: var(--ink-2); background: var(--paper);
}
.empty strong { display: block; color: var(--ink); margin-bottom: 6px; }

/* display:block is load-bearing. These are often <a> elements wrapping
   <div> children. An inline element containing block children makes the
   browser fragment its background into pieces that paint in the wrong
   place, which showed up as stray white rectangles down the left edge. */
.card-block {
  display: block;
  background: var(--paper); border: 1px solid var(--rule);
  border-radius: 12px; padding: 16px; margin-bottom: 12px;
}

label { display: block; font-size: 14px; color: var(--ink-2); margin-bottom: 8px; }

input[type="text"] {
  width: 100%; min-height: var(--tap); padding: 12px 14px;
  font-size: 19px; font-family: var(--sans);
  letter-spacing: 0.14em; text-transform: uppercase;
  background: var(--paper); border: 1px solid var(--rule);
  border-radius: 10px; color: var(--ink);
}

button {
  width: 100%; min-height: var(--tap); margin-top: 14px; padding: 13px;
  font-family: var(--sans); font-size: 17px; font-weight: 650;
  background: var(--accent); color: #fff; border: none; border-radius: 10px;
}

.error {
  margin-top: 12px; padding: 11px 14px; border-radius: 10px; font-size: 15px;
  background: #FDECEC; border: 1px solid #F5B5B5; color: #8A1F1F;
}

@media (prefers-reduced-motion: reduce) {
  * { animation: none !important; transition: none !important; }
}

/* ---------- Artists ---------- */

.card-block.artist { display: flex; gap: 13px; align-items: center; }
.card-block.artist img {
  width: 58px; height: 58px; border-radius: 50%;
  object-fit: cover; flex: 0 0 auto; background: var(--paper-2);
}
.artist-name { font-size: 17.5px; font-weight: 620; }
.artist-teaser { font-size: 14px; color: var(--ink-2); margin-top: 2px; }

.artist-hero {
  width: 100%; max-width: 260px; aspect-ratio: 1; object-fit: cover;
  border-radius: 12px; display: block; margin: 0 auto 16px;
}

/* Sched bios keep bold, italics and breaks; their fonts are stripped
   on import so the text inherits ours. */
.bio { font-size: 16px; line-height: 1.55; }
.bio strong { font-weight: 650; }

h2.sect {
  font-size: 12.5px; text-transform: uppercase; letter-spacing: 0.09em;
  color: var(--ink-2); margin: 22px 0 8px;
}

/* ============================================================
   Cruise picker
   Row-per-sailing with the porthole logo, matching the way
   guests already recognise these programs from the website.
   ============================================================ */

.cruise-list { padding-bottom: 20px; }

.group-label {
  padding: 14px 16px 7px;
  font-size: 12px; font-weight: 700;
  text-transform: uppercase; letter-spacing: 0.1em;
  color: var(--ink-2); background: var(--paper-2);
}

.cruise-row-form { margin: 0; }

.cruise-row {
  width: 100%; display: flex; align-items: center; gap: 14px;
  padding: 13px 16px; margin: 0;
  background: var(--paper); border: none;
  border-bottom: 1px solid var(--rule);
  text-align: left; font: inherit; color: inherit;
  min-height: 82px; cursor: pointer; border-radius: 0;
}
.cruise-row:active { background: var(--paper-2); }

.cruise-row img,
.cruise-row .logo-fallback {
  width: 62px; height: 62px; border-radius: 12px;
  flex: 0 0 auto; object-fit: cover; background: var(--paper-2);
}
.cruise-row .logo-fallback {
  display: flex; align-items: center; justify-content: center;
  font-weight: 700; font-size: 20px; color: var(--ink-2);
  border: 1px solid var(--rule);
}

.cruise-meta { flex: 1 1 auto; min-width: 0; }
.cruise-name {
  display: block; font-size: 17.5px; font-weight: 620;
  letter-spacing: -0.01em; line-height: 1.25;
}
.cruise-dates {
  display: block; margin-top: 3px;
  font-size: 14.5px; color: var(--ink-2);
}

.cruise-row .chev {
  flex: 0 0 auto; font-size: 22px; color: var(--ink-3); line-height: 1;
}

/* Locked and past sailings stay legible — dimmed, never hidden.
   Guests should see their cruise exists before it opens. */
.cruise-row.locked img,
.cruise-row.locked .logo-fallback { opacity: .45; }
.cruise-row.locked .cruise-name { color: var(--ink-2); }
.cruise-row.locked .chev { font-size: 15px; }
.cruise-row.past img { opacity: .6; }

/* ---------- Bottom sheet ---------- */

.sheet-scrim {
  position: fixed; inset: 0; z-index: 90;
  background: rgba(10,14,20,.42);
}

.sheet {
  position: fixed; z-index: 91;
  left: 0; right: 0; bottom: 0;
  background: var(--paper);
  border-radius: 18px 18px 0 0;
  padding: 24px 22px calc(24px + env(safe-area-inset-bottom));
  box-shadow: 0 -6px 34px rgba(0,0,0,.2);
}
.sheet h2 { margin: 0 0 8px; font-size: 20px; letter-spacing: -0.01em; }
.sheet p { margin: 0 0 6px; font-size: 16px; line-height: 1.5; }
.sheet .sheet-sub { color: var(--ink-2); font-size: 14.5px; margin-bottom: 18px; }

.btn-primary {
  width: 100%; min-height: var(--tap); padding: 13px;
  font-family: var(--sans); font-size: 17px; font-weight: 650;
  background: var(--accent); color: #fff;
  border: none; border-radius: 10px; cursor: pointer;
}

@media (min-width: 560px) {
  .sheet {
    left: 50%; bottom: 50%; right: auto;
    transform: translate(-50%, 50%);
    width: 420px; border-radius: 16px;
  }
}

/* ============================================================
   Account, notifications, chat
   ============================================================ */

.nav.nav-4 { grid-template-columns: repeat(4, 1fr); }
.nav a { position: relative; }

.badge {
  position: absolute; top: 8px; right: 50%; transform: translateX(20px);
  min-width: 18px; height: 18px; padding: 0 5px;
  border-radius: 9px; background: var(--red-card); color: #fff;
  font-size: 11px; font-weight: 700; line-height: 18px; text-align: center;
}
.badge.inline { position: static; transform: none; margin-left: auto; }

/* Flash messages.

   These used to sit in the normal flow at the very top of the page, so
   a sign-in error appeared above the fold you were looking at and you
   had to scroll up to find out anything was wrong -- and on a phone
   with a Dynamic Island the text ran underneath it and was cut in half.

   Fixed to the top, above everything, and padded clear of the island. */
.toast {
  position: fixed; top: 0; left: 0; right: 0; z-index: 120;
  padding: 12px 16px;
  padding-top: calc(12px + env(safe-area-inset-top));
  background: #E8F4EA; border-bottom: 1px solid #BFDCC6;
  font-size: 15px; line-height: 1.45; color: #1E4527;
  box-shadow: 0 2px 10px rgba(0,0,0,.10);
  transition: opacity .3s ease, transform .3s ease;
}
.toast p { margin: 0; }
.toast p + p { margin-top: 6px; }
.toast.gone { opacity: 0; transform: translateY(-8px); pointer-events: none; }

/* Errors need to look different from confirmations. Both were green. */
.toast.is-error {
  background: #FDEBEA; border-bottom-color: #F0C2BE; color: #7A1F17;
}

.toast-close {
  position: absolute; top: calc(6px + env(safe-area-inset-top)); right: 8px;
  width: 32px; height: 32px; min-height: 32px; margin: 0; padding: 0;
  border: 0; background: none; color: inherit; font-size: 20px; line-height: 1;
  opacity: .6;
}

/* ---------- Profile header ---------- */

.profile {
  display: flex; align-items: center; gap: 14px;
  padding: 4px 0 20px;
}
.avatar {
  width: 62px; height: 62px; border-radius: 50%;
  background: var(--accent); color: #fff;
  display: flex; align-items: center; justify-content: center;
  font-size: 26px; font-weight: 650; flex: 0 0 auto;
}
.avatar.anon { background: var(--rule); color: var(--ink-2); font-size: 27px; }
.profile-name { font-size: 19px; font-weight: 650; letter-spacing: -0.01em; }
.profile-sub { font-size: 14.5px; color: var(--ink-2); margin-top: 2px; }

/* ---------- Settings rows ---------- */

.rows {
  background: var(--paper); border: 1px solid var(--rule);
  border-radius: 12px; overflow: hidden; margin-bottom: 22px;
}
.row {
  width: 100%; display: flex; align-items: center; gap: 13px;
  padding: 14px 15px; min-height: 56px;
  background: var(--paper); border: none;
  border-bottom: 1px solid var(--rule);
  font: inherit; color: inherit; text-align: left; cursor: pointer;
}
.rows .row:last-child { border-bottom: none; }
.row:active { background: var(--paper-2); }
.row[disabled] { opacity: .6; cursor: default; }
.row-ic { font-size: 20px; line-height: 1; flex: 0 0 auto; }
.row-label { flex: 1 1 auto; font-size: 16.5px; }
.row-chev { color: var(--ink-3); font-size: 20px; line-height: 1; }

.btn-quiet {
  width: 100%; min-height: var(--tap); padding: 13px;
  font-family: var(--sans); font-size: 16.5px; font-weight: 600;
  background: var(--paper); color: var(--red-card);
  border: 1px solid var(--rule); border-radius: 10px; cursor: pointer;
}

.note { font-size: 14.5px; color: var(--ink-2); margin: 0 0 12px; }
.hint { text-transform: none; letter-spacing: 0; color: var(--ink-3); }

.card-block label { margin-top: 14px; }
.card-block label:first-child { margin-top: 0; }
.card-block input[type="text"],
.card-block input[type="email"],
.card-block input[type="password"] {
  width: 100%; min-height: var(--tap); padding: 12px 14px;
  font-size: 17px; font-family: var(--sans);
  letter-spacing: normal; text-transform: none;
  background: var(--paper); border: 1px solid var(--rule);
  border-radius: 10px; color: var(--ink);
}

/* ---------- Notifications ---------- */

.notif-title { font-size: 17px; font-weight: 620; }
.notif-body { font-size: 15.5px; margin-top: 4px; line-height: 1.5; }
.notif-time { font-size: 13.5px; color: var(--ink-3); margin-top: 8px; }
.notif-link {
  display: inline-block; margin-top: 8px;
  font-size: 15px; font-weight: 600; color: var(--accent-ink);
}

.empty.coming strong { font-size: 18px; }

/* ---------- Sheet steps ---------- */

.steps { margin: 4px 0 12px; padding-left: 22px; }
.steps li { margin-bottom: 9px; font-size: 16px; line-height: 1.45; }

/* ---------- Back button in the header ---------- */

.topbar-back {
  display: flex; align-items: flex-start; justify-content: space-between; gap: 14px;
}
.topbar-back h1 { margin-right: 0; }

.back-btn {
  flex: 0 0 auto; display: inline-flex; align-items: center; gap: 4px;
  min-height: 36px; padding: 7px 12px 7px 9px;
  border: 1px solid var(--rule); border-radius: 999px;
  background: var(--paper); color: var(--ink-2);
  font-size: 14.5px; font-weight: 600;
}
.back-btn:active { background: var(--paper-2); }

/* ---------- Row completion tick ---------- */

.row-done {
  flex: 0 0 auto; margin-left: auto;
  font-size: 17px; font-weight: 700; line-height: 1;
  color: #0E8A2E;
}
.row[disabled] .row-label { color: var(--ink-2); }

/* Nav glyphs are inline SVG now, not emoji. */
.nav .glyph { display: flex; align-items: center; justify-content: center; }
.avatar.anon { background: var(--rule); }

/* ============================================================
   My Schedule, chat
   ============================================================ */

.nav.nav-5 { grid-template-columns: repeat(5, 1fr); }
.nav.nav-5 a { font-size: 11px; }

/* ---------- Star ---------- */

.slot { grid-template-columns: var(--rail) 1fr auto; }

.star {
  align-self: center; flex: 0 0 auto;
  width: 44px; height: 44px; padding: 0;
  display: flex; align-items: center; justify-content: center;
  background: none; border: none; cursor: pointer; margin: 0;
  min-height: 44px;
}
.star svg { fill: none; stroke: var(--ink-3); stroke-width: 1.4; }
.star.on svg { fill: var(--learn); stroke: var(--learn); }

.day-head {
  padding: 16px 0 6px calc(var(--rail) + 12px);
  font-size: 12px; font-weight: 700;
  text-transform: uppercase; letter-spacing: 0.1em; color: var(--ink-2);
}

.clash {
  margin-top: 7px; padding: 5px 9px; border-radius: 7px;
  background: #FDF0E6; border: 1px solid #F0C9A8;
  font-size: 13px; color: #8A4B1F;
}

.btn-primary.block { display: block; text-align: center; text-decoration: none; }

/* ---------- Chat list ---------- */

.row.thread { gap: 12px; }
.thread-av {
  width: 42px; height: 42px; border-radius: 50%;
  background: var(--rule); color: var(--ink-2);
  display: flex; align-items: center; justify-content: center;
  font-weight: 650; font-size: 17px; flex: 0 0 auto;
}
.thread-av.lounge { background: var(--accent); color: #fff; }
.thread-main { flex: 1 1 auto; min-width: 0; }
.thread-title { display: block; font-size: 16.5px; font-weight: 600; }
.thread-preview {
  display: block; font-size: 14px; color: var(--ink-2);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}

.search { margin-bottom: 14px; }
.search input {
  width: 100%; min-height: var(--tap); padding: 12px 14px;
  font-size: 16.5px; font-family: var(--sans);
  border: 1px solid var(--rule); border-radius: 10px;
  background: var(--paper); color: var(--ink);
}

/* ---------- Thread ---------- */

.thread-scroll {
  padding: 14px 16px 8px;
  overflow-y: auto;
  -webkit-overflow-scrolling: touch;
  /* leave room for the composer, the tools row and the tab bar */
  max-height: calc(100vh - 260px);
}

.msg { margin-bottom: 12px; max-width: 82%; }
.msg.mine { margin-left: auto; text-align: right; }

.msg-name {
  font-size: 12.5px; font-weight: 650; color: var(--ink-2);
  margin-bottom: 3px; padding-left: 3px;
}
.bubble {
  display: inline-block; text-align: left;
  padding: 10px 13px; border-radius: 16px;
  background: var(--paper); border: 1px solid var(--rule);
  font-size: 16px; line-height: 1.45; white-space: pre-wrap;
  overflow-wrap: anywhere;
}
.msg.mine .bubble {
  background: var(--accent); border-color: var(--accent); color: #fff;
}
.bubble.gone {
  background: transparent; border-style: dashed;
  color: var(--ink-3); font-style: italic;
}
.msg.mine .bubble.gone { color: var(--ink-3); }
.msg-time { font-size: 11.5px; color: var(--ink-3); margin-top: 3px; padding: 0 4px; }

.composer {
  flex: 0 0 auto;
  display: flex; gap: 8px; align-items: flex-end;
  padding: 9px 12px;
  padding-bottom: calc(9px + env(safe-area-inset-bottom));
  background: var(--paper); border-top: 1px solid var(--rule);
}
/* The keyboard covers the home indicator, so its inset is dead space. */
html.kb-open .composer { padding-bottom: 9px; }
.composer textarea {
  flex: 1 1 auto; resize: none;
  min-height: 42px; max-height: 120px; padding: 10px 13px;
  font-family: var(--sans); font-size: 16px; line-height: 1.35;
  border: 1px solid var(--rule); border-radius: 20px;
  background: var(--paper-2); color: var(--ink);
}
.composer button {
  flex: 0 0 auto; width: 42px; height: 42px; min-height: 42px;
  margin: 0; padding: 0; border-radius: 50%;
  display: flex; align-items: center; justify-content: center;
  background: var(--accent); color: #fff; border: none;
}

.link-btn {
  width: auto; margin: 0; padding: 4px 6px; min-height: 32px;
  background: none; border: none; cursor: pointer;
  font-family: var(--sans); font-size: 13px; color: var(--ink-3);
  text-decoration: underline;
}
.link-btn.danger { color: var(--red-card); }

.sheet textarea {
  width: 100%; padding: 11px 13px; margin-bottom: 4px;
  font-family: var(--sans); font-size: 16px;
  border: 1px solid var(--rule); border-radius: 10px;
  background: var(--paper); color: var(--ink); resize: vertical;
}
.sheet .check { margin: 10px 0 14px; }

/* ---------- Event detail ---------- */

.event-head {
  display: flex; align-items: flex-start; gap: 12px;
  border-left: 5px solid var(--information);
}
.event-head[data-cat="enjoy"]      { border-left-color: var(--enjoy); }
.event-head[data-cat="in_concert"] { border-left-color: var(--in-concert); }
.event-head[data-cat="learn"]      { border-left-color: var(--learn); }
.event-head[data-seat="red"]       { border-left-color: var(--red-card); }
.event-head[data-seat="blue"]      { border-left-color: var(--blue-card); }
.event-head-main { flex: 1 1 auto; min-width: 0; }

.kv-label {
  font-size: 12.5px; text-transform: uppercase; letter-spacing: 0.08em;
  color: var(--ink-2); margin-top: 12px;
}
.kv-label:first-child { margin-top: 0; }
.kv-value { font-size: 17px; font-weight: 600; margin-top: 2px; }

.star.big { width: 52px; height: 52px; }

/* ---------- Performer rows ---------- */

.row.performer { gap: 12px; }
.row.performer img {
  width: 44px; height: 44px; border-radius: 50%;
  object-fit: cover; background: var(--paper-2); flex: 0 0 auto;
}
.performer-role {
  display: block; font-size: 13.5px; color: var(--ink-2);
  text-transform: capitalize;
}

/* ---------- Artist "Appearing" rows with a star ---------- */

.set-row { display: flex; align-items: center; gap: 10px; padding: 12px 14px; }
.set-main { flex: 1 1 auto; min-width: 0; }
.set-title { font-size: 16.5px; font-weight: 620; }
.set-meta { font-size: 14px; color: var(--ink-2); margin-top: 2px; }
.set-row.struck .set-title { text-decoration: line-through; color: var(--ink-3); }

/* ---------- Sign-in sheet ---------- */

#signin-sheet .link-btn {
  display: block; width: 100%; margin-top: 12px; text-align: center;
  font-size: 15px;
}

/* ---------- Day info: theme and port ---------- */

.dayinfo {
  padding: 10px 16px;
  background: var(--paper);
  border-bottom: 1px solid var(--rule);
}
.dayinfo-row {
  display: flex; align-items: baseline; gap: 10px;
  font-size: 15px; padding: 2px 0;
}
.dayinfo-row strong {
  flex: 0 0 46px;
  font-size: 11.5px; font-weight: 700;
  text-transform: uppercase; letter-spacing: 0.09em;
  color: var(--ink-2);
}
.dayinfo-row span { color: var(--ink); font-weight: 550; }
.dayinfo-row .at-sea { color: var(--ink-2); font-style: italic; font-weight: 400; }

/* ---------- Staff preview ---------- */

.staff-bar {
  padding: 9px 16px;
  background: #2E2A12; color: #F5E6B8;
  font-size: 13.5px; font-weight: 600;
  display: flex; align-items: center; justify-content: space-between; gap: 12px;
}
.staff-bar a { color: #FFD84D; text-decoration: underline; flex: 0 0 auto; }

#staff-toggle { display: block; width: 100%; margin-top: 14px; text-align: center; }
#staff-form { margin-top: 12px; text-align: left; }
#staff-form label {
  display: block; font-size: 12.5px; text-transform: uppercase;
  letter-spacing: 0.08em; color: var(--ink-2); margin-bottom: 6px;
}
#staff-form input {
  width: 100%; min-height: var(--tap); padding: 12px 14px;
  font-size: 18px; font-family: var(--sans);
  letter-spacing: 0.14em; text-transform: uppercase;
  border: 1px solid var(--rule); border-radius: 10px;
  background: var(--paper); color: var(--ink);
}
#staff-form button {
  width: 100%; min-height: var(--tap); margin-top: 10px; padding: 12px;
  font-family: var(--sans); font-size: 16.5px; font-weight: 650;
  background: var(--ink); color: #fff; border: none; border-radius: 10px;
}

/* ============================================================
   Layout corrections
   ============================================================ */

/* The time rail: the node on the spine was sitting close enough to the
   clock numerals to clip the last digit. Widen the gutter and push the
   spine and node right so the time always has clear room. */
:root { --rail: 78px; }

.slot .time { padding-right: 22px; }

.rail::before { left: calc(16px + var(--rail) - 9px); }
.slot::after  { left: calc(var(--rail) - 13px); }

/* Keep the tab bar in exactly the same place on every page.

   Chat and My Schedule are short enough not to scroll, and on iOS a
   non-scrolling page reports a different viewport height — so the bar
   appeared to jump up. Giving every page the same minimum height stops
   that. 100dvh follows the browser chrome as it collapses; the 100vh
   line before it is the fallback for older browsers. */
body {
  min-height: 100vh;
  min-height: 100dvh;
}

main { min-height: 1px; }
/* The app moves focus to each new screen for screen readers; that is
   not something to draw a ring around. */
#screen:focus { outline: none; }

.nav {
  position: fixed;
  bottom: 0;
  height: calc(60px + env(safe-area-inset-bottom));
}

/* The thread view scrolls inside itself, so it needs the tab bar,
   composer and tools accounted for — but never more than the page. */
.thread-scroll {
  max-height: calc(100dvh - 268px);
}

/* ---------- Staff access ---------- */

.staff-entry {
  padding: 26px 16px calc(24px + env(safe-area-inset-bottom));
  text-align: center;
}
.staff-entry #staff-toggle {
  width: auto; margin: 0; padding: 6px 10px; min-height: 32px;
  background: none; border: none; cursor: pointer;
  font-family: var(--sans); font-size: 13px; color: var(--ink-3);
  text-decoration: underline;
}
.staff-entry form {
  max-width: 320px; margin: 12px auto 0;
  text-align: left;
  background: var(--paper); border: 1px solid var(--rule);
  border-radius: 12px; padding: 16px;
}

/* ---------- Refresh ---------- */

#refresh-btn.spinning svg { animation: spin .8s linear infinite; }
@keyframes spin { to { transform: rotate(360deg); } }

@media (prefers-reduced-motion: reduce) {
  #refresh-btn.spinning svg { animation: none; }
}

/* ============================================================
   Layout, round two
   ============================================================ */

/* Nothing may push the page sideways. A long sailing name next to a
   header button was doing exactly that.

   NOTE: this must be `clip`, not `hidden`. overflow-x:hidden makes the
   element a scroll container, and that silently disables position:sticky
   for everything inside it -- which is exactly why the frozen schedule
   header scrolled away. `clip` crops without creating a scroll box. */
html, body { max-width: 100%; overflow-x: clip; }

.topbar-back { gap: 10px; }
.topbar-back > div { min-width: 0; flex: 1 1 auto; }
.topbar-back h1 { overflow-wrap: anywhere; }

/* A small square icon button, for actions that don't need a word. */
.icon-btn {
  flex: 0 0 auto;
  width: 36px; height: 36px; min-height: 36px;
  margin: 0; padding: 0;
  display: flex; align-items: center; justify-content: center;
  background: var(--paper); color: var(--ink-2);
  border: 1px solid var(--rule); border-radius: 50%;
  cursor: pointer;
}
.icon-btn:active { background: var(--paper-2); }

/* ---------- More room for the events themselves ----------
   The page had 16px of margin either side plus a wide time gutter,
   which left little for long show titles. Tighter throughout. */

:root { --rail: 62px; }

.rail { padding-left: 8px; padding-right: 8px; }
.slot { gap: 6px; padding: 7px 0; }
.slot .time { padding-right: 10px; font-size: 17.5px; }
.slot .time .ampm { font-size: 10px; }
.slot .card { padding: 10px 11px; }
.slot .title { font-size: 16.5px; }
.slot .meta { font-size: 13.5px; }

/* The spine sits between the time column and the card, clear of both. */
.rail::before { left: calc(8px + var(--rail) - 5px); }
.slot::after  { left: calc(var(--rail) - 9px); }

.wrap { padding-left: 12px; padding-right: 12px; }
.topbar { padding-left: 12px; padding-right: 12px; }
.days { padding-left: 12px; padding-right: 12px; }
.dayinfo { padding-left: 12px; padding-right: 12px; }
.legend { padding-left: 12px; padding-right: 12px; }
.day-head { padding-left: calc(var(--rail) + 16px); }

/* ---------- The tab bar that wouldn't stay put ----------
   Chat and My Schedule are short enough not to scroll. In mobile
   Safari a page that never scrolls keeps the browser's bottom toolbar
   expanded, which shortens the visual viewport — so a bottom-fixed bar
   sits higher there than on pages that do scroll.
   Making every page a fraction taller than the viewport means the page
   is always scrollable, so the chrome behaves the same everywhere. */
body {
  min-height: calc(100vh + 1px);
  min-height: calc(100dvh + 1px);
}

/* ---------- Count badge on a header icon ---------- */

.icon-btn { position: relative; }

.icon-badge {
  position: absolute; top: -3px; right: -3px;
  min-width: 17px; height: 17px; padding: 0 4px;
  border-radius: 9px;
  background: var(--red-card); color: #fff;
  font-size: 10.5px; font-weight: 700; line-height: 17px;
  text-align: center;
  /* A ring in the page colour keeps the count legible where it
     overlaps the button's own border. */
  box-shadow: 0 0 0 2px var(--paper);
}

/* Two header buttons sit side by side without crowding the title. */
.topbar-back { align-items: center; }
.topbar-back .icon-btn + .icon-btn { margin-left: 2px; }

/* Switch Cruise sits on its own at the very bottom — it's the one
   action here that leaves the sailing entirely, so it's styled as a
   deliberate action rather than another settings row. */
.btn-switch {
  display: block; width: 100%;
  margin: 28px 0 8px;
  padding: 14px; min-height: var(--tap);
  background: var(--red-card); color: #fff;
  border: none; border-radius: 10px;
  font-family: var(--sans); font-size: 17px; font-weight: 650;
  text-align: center; text-decoration: none;
}
.btn-switch:active { filter: brightness(.92); }

/* ---------- In-app alert ----------
   Shown when a message arrives while the app is open. This is the
   fallback for guests whose phone can't hold a push connection — on a
   ship that blocks Google's FCM ports, that's every Android device. */

.live-alert {
  position: fixed; z-index: 95;
  left: 12px; right: 12px;
  top: calc(12px + env(safe-area-inset-top));
  padding: 13px 16px;
  background: var(--accent); color: #fff;
  border-radius: 12px;
  font-size: 15.5px; font-weight: 600;
  box-shadow: 0 6px 22px rgba(0,0,0,.22);
  text-align: center;
  transform: translateY(-140%);
  transition: transform .28s ease;
}
.live-alert.show { transform: translateY(0); }

@media (prefers-reduced-motion: reduce) {
  .live-alert { transition: none; }
}

/* A guest's own entries read differently from the programme — dashed
   edge and a muted band, so the daily schedule still leads. */
.slot[data-cat="personal"] .card {
  border-left-style: dashed;
  border-left-color: var(--ink-3);
  background: var(--paper-2);
}

.row2 { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; }
.card-block input[type="time"], .card-block select, .card-block textarea {
  width: 100%; min-height: var(--tap); padding: 12px 14px;
  font-size: 17px; font-family: var(--sans);
  background: var(--paper); border: 1px solid var(--rule);
  border-radius: 10px; color: var(--ink);
}
.card-block textarea { min-height: 0; resize: vertical; }

.btn-quiet.block { display: block; text-align: center; }

/* ---------- Search ---------- */

.searchbar {
  display: flex; align-items: center; gap: 9px;
  padding: 10px 12px;
  background: var(--paper);
  border-bottom: 1px solid var(--rule);
}
.searchbar svg { flex: 0 0 auto; color: var(--ink-3); }
.searchbar input {
  flex: 1 1 auto; min-width: 0;
  min-height: 40px; padding: 8px 4px;
  font-size: 16.5px; font-family: var(--sans);
  /* 16px+ stops iOS zooming the page when the field is focused. */
  background: none; border: none; color: var(--ink);
  -webkit-appearance: none;
}
.searchbar input:focus { outline: none; }
.searchbar input::-webkit-search-cancel-button { display: none; }

.search-clear {
  flex: 0 0 auto;
  width: 30px; height: 30px; border-radius: 50%;
  display: flex; align-items: center; justify-content: center;
  background: var(--paper-2); color: var(--ink-2);
  font-size: 19px; line-height: 1; text-decoration: none;
}

.search-summary {
  padding: 10px 12px;
  background: var(--paper-2);
  border-bottom: 1px solid var(--rule);
  font-size: 13.5px; color: var(--ink-2);
}

/* Search run on the phone's own copy, with no connection. Said quietly,
   so a guest knows why the results might be a little behind. */
.offline-search-note {
  padding: 6px 12px;
  font-size: 12.5px; color: var(--ink-2);
  background: var(--paper-2);
  border-bottom: 1px solid var(--rule);
}


/* ---------- Offline notice ----------
   Sits above everything, including the staff bar, because it changes
   how you should read every other thing on the page. */
.offline-bar {
  /* A slim strip just above the tab bar. At the top it sat under the
     iPhone's clock and battery, and pushed the header down a line. */
  position: fixed; left: 0; right: 0; z-index: 29;
  bottom: calc(61px + env(safe-area-inset-bottom));
  padding: 7px 16px;
  background: #F3895B; color: #1B1206;
  font-size: 13.5px; font-weight: 600; line-height: 1.35;
  text-align: center;
  box-shadow: 0 -1px 6px rgba(0,0,0,.12);
}
/* No tab bar (the cruise picker): sit at the very bottom. */
body:not(:has(.nav)) .offline-bar { bottom: env(safe-area-inset-bottom); }
/* In a conversation the composer lives there; the offline sheet and the
   send failure message say enough. */
html:has(.chat-shell) .offline-bar { display: none; }
/* Keep the end of every screen clear of the strip. */
html.is-offline #screen { padding-bottom: 36px; }
.offline-bar[hidden] { display: none; }


/* ---------- Frozen schedule header ----------
   Title, day tabs and search stay put so a guest can jump days from
   anywhere in a long list without scrolling back to the top. The
   children were individually sticky, which stacked them all at top:0
   and overlapped them; sticking the wrapper keeps their natural order. */
.sched-head {
  position: sticky; top: 0; z-index: 30;
  background: var(--paper);
  border-bottom: 1px solid var(--rule);
}
.sched-head .topbar,
.sched-head .days,
.sched-head .searchbar { position: static; border-bottom: 0; }
.sched-head .days,
.sched-head .searchbar { border-top: 1px solid var(--rule); }

/* On a short screen the frozen block could eat the page. Tighten it. */
@media (max-height: 460px) {
  .sched-head .topbar { padding-top: calc(6px + env(safe-area-inset-top));
                        padding-bottom: 6px; }
  .sched-head .days { padding-top: 6px; padding-bottom: 6px; }
}

/* Date sits at the right end of the port row. */
.dayinfo-row .dayinfo-date {
  margin-left: auto; padding-left: 12px;
  color: var(--ink-2); font-size: 14px; font-weight: 600;
  white-space: nowrap;
}


/* ---------- Saved confirmation ---------- */
.saved-note {
  display: flex; gap: 10px; align-items: flex-start;
  margin: 0 0 12px; padding: 12px 14px;
  border: 1px solid rgba(7,168,0,.35); border-radius: 12px;
  background: rgba(7,168,0,.10);
  font-size: 14.5px; line-height: 1.45;
  transition: opacity .6s ease;
}
.saved-note.fading { opacity: 0; }
.saved-tick {
  flex: 0 0 auto; width: 20px; height: 20px; border-radius: 50%;
  background: #0a8f00; color: #fff;
  display: flex; align-items: center; justify-content: center;
  font-size: 13px; font-weight: 700; line-height: 1;
}

/* Settled profile view, shown until the guest taps Edit. */
.kv { display: flex; justify-content: space-between; gap: 14px; padding: 7px 0; }
.kv + .kv { border-top: 1px solid var(--rule); }
.kv .k { color: var(--ink-2); font-size: 14px; }
.kv .v { font-weight: 600; font-size: 15px; text-align: right; }
#profile-view .btn-quiet { margin-top: 12px; }


/* The hidden attribute has to beat class rules that set display.
   .card-block sets display:block, which was overriding [hidden] and
   leaving the profile editor open next to its own collapsed view. */
[hidden] { display: none !important; }


/* ---------- Navigation feedback ----------
   Pages are server-rendered, so a tap is a full page load. Without an
   immediate response the app reads as frozen for a second and a half.
   None of this changes how fast anything is -- only whether the guest
   can tell the tap registered. */
.navbar-progress {
  position: fixed; top: 0; left: 0; height: 3px; width: 0;
  z-index: 200; background: var(--accent);
  transition: width 900ms cubic-bezier(.2,.7,.3,1);
  pointer-events: none;
}

/* The tapped item dims instantly, before any network work begins. */
.is-tapped { opacity: .55; transition: opacity 80ms linear; }

/* :active covers the press itself; .is-tapped holds it through the wait. */
.days a.day:active,
.nav a:active,
.slot:active { opacity: .55; }

/* iOS otherwise waits ~100ms to see if a tap becomes a scroll before
   painting :active, which is most of why the press felt unregistered. */
.days a.day, .nav a, .slot, button { -webkit-tap-highlight-color: transparent; }

@media (prefers-reduced-motion: reduce) {
  .navbar-progress { transition: none; }
  .is-tapped { transition: none; }
}


/* ============================================================
   Chat thread — one shell, not three floating bars
   ============================================================
   Header, messages and composer were each position:fixed at hardcoded
   offsets. Report/Block sat at 122px with no background, so messages
   scrolled underneath it, and every offset was wrong the moment the
   keyboard changed the viewport height. One flex column fixes all of it.

   --kb is the keyboard height, measured from visualViewport in JS. */
.chat-shell {
  /* Height and vertical offset are set from visualViewport in JS.
     iOS does not shrink the LAYOUT viewport for the keyboard, so a
     full-height fixed element runs on behind it -- and iOS then scrolls
     the whole fixed layer up to reveal the focused field, which is what
     pushed the header off the top of the screen and dragged the tab bar
     into view above the keyboard. Sizing to the VISUAL viewport instead
     keeps the shell exactly over what the guest can actually see. */
  position: fixed; left: 0; right: 0; top: 0;
  height: 100%;
  display: flex; flex-direction: column;
  background: var(--paper-2);
  overflow: hidden;
}
.chat-shell .chat-head { flex: 0 0 auto; }
.chat-shell .thread-scroll {
  flex: 1 1 auto; min-height: 0;
  overflow-y: auto; -webkit-overflow-scrolling: touch;
  overscroll-behavior: contain;
  padding: 12px 0 8px;
}

/* Leave room for the tab bar, and reclaim it when the keyboard is up —
   the tab bar is hidden then, so the space is ours. */
.chat-shell { padding-bottom: calc(60px + env(safe-area-inset-bottom)); }
html.kb-open .chat-shell { padding-bottom: 0; }

/* The tab bar competing with the keyboard left a sliver of chat visible.
   In a conversation the keyboard wins. */
html.kb-open .nav { display: none; }

.chat-head-main { min-width: 0; flex: 1 1 auto; }

/* `.topbar-back > div { flex: 1 1 auto }` above is (0,1,1) -- an element
   selector beats a plain class -- so `.chat-head-actions` never took
   effect and the button group stretched, pushing Back inward. Match the
   element too so this actually wins. */
.topbar-back > div.chat-head-actions {
  display: flex; align-items: center; gap: 8px;
  flex: 0 0 auto; margin-left: auto;
}

/* The menu is positioned against the header, so the header has to be the
   containing block. It previously resolved against .chat-shell, which is
   the whole screen -- top:100% put the menu below the viewport, which is
   why tapping the button appeared to do nothing at all. */
.chat-head { position: relative; }

/* Two older rules cap this at `max-height: calc(100dvh - 268px)`. The
   shell rule below is more specific, but specificity does not touch a
   property you never set -- so the cap survived and the flex layout
   never took hold. */
.chat-shell .thread-scroll {
  max-height: none; height: auto; position: static;
  padding: 12px 16px 8px;
}

/* Same story: the page-height hack that keeps mobile Safari's toolbar
   steady is pointless inside an installed app, where there is no
   toolbar -- and the extra pixel is exactly what lets a short page
   rubber-band under the fixed tab bar. */
@media (display-mode: standalone) {
  body { min-height: 100dvh; }
}
.icon-btn {
  width: 36px; height: 36px; min-height: 36px; margin: 0; padding: 0;
  display: flex; align-items: center; justify-content: center;
  background: none; border: 1px solid var(--rule); border-radius: 50%;
  color: var(--ink-2); cursor: pointer;
}

/* Report and Block: a menu off the header, so they can never sit on top
   of the conversation. */
.thread-menu {
  position: absolute; top: calc(100% - 4px); right: 12px; z-index: 40;
  display: flex; flex-direction: column; align-items: stretch; gap: 2px;
  padding: 6px; min-width: 200px;
  background: var(--paper); border: 1px solid var(--rule);
  border-radius: 12px; box-shadow: 0 8px 26px rgba(0,0,0,.16);
}
.thread-menu .link-btn { text-align: left; text-decoration: none; padding: 9px 10px; }
.thread-menu form { margin: 0; }

/* Safari inflates text in narrow columns before layout settles, which is
   why the bubbles appeared huge and then snapped smaller. */
html { -webkit-text-size-adjust: 100%; text-size-adjust: 100%; }

/* Rubber-band scrolling on a short page made the fixed tab bar look like
   it was sliding. Stop the bounce. */
body { overscroll-behavior-y: none; }


/* ---------- Destructive actions ---------- */
.danger-panel {
  margin: 0 0 16px; padding: 16px 18px;
  border: 1px solid rgba(214,0,0,.35); border-radius: 14px;
  background: rgba(214,0,0,.06);
}
.danger-panel h2 { margin: 0 0 8px; font-size: 18px; color: #B00000; }
.danger-panel p { margin: 0 0 10px; font-size: 15px; line-height: 1.5; }
.danger-panel ul { margin: 0 0 12px; padding-left: 20px; font-size: 15px; line-height: 1.7; }
.danger-panel .sub { color: var(--ink-2); font-size: 14px; }
.btn-danger {
  background: #B00000; color: #fff; border: 0;
}
.btn-danger:disabled { opacity: .45; }
.btn-quiet.block { display: block; text-align: center; text-decoration: none; margin-top: 10px; }


/* ---------- Artist bio, clamped ---------- */
.bio-text.is-clamped {
  display: -webkit-box;
  -webkit-line-clamp: 5;
  line-clamp: 5;
  -webkit-box-orient: vertical;
  overflow: hidden;
}
/* The bio can contain <p> from the import; a box-clamp only counts the
   lines of its own children, so collapse the spacing between them. */
.bio-text.is-clamped > p { margin: 0; display: inline; }
.bio-text.is-clamped > p + p::before { content: " "; }
.bio-more {
  margin: 10px 0 0; padding: 0; min-height: 0;
  background: none; border: 0;
  color: var(--accent-ink, #2900F3); font-size: 15px; font-weight: 600;
}


/* ---------- Cruise header artwork ----------
   The sailing name lives in the image, so there is no text over it.
   Full width, aspect preserved, never cropped -- a title that gets
   clipped on a narrow phone is worse than a smaller one. */
.topbar-art {
  position: relative;
  display: block; padding: 0;
  /* The status bar sits ABOVE the artwork, not over it. Bleeding the
     image under the Dynamic Island would eat the top of the title. */
  padding-top: env(safe-area-inset-top);
  background: var(--paper);
  border-bottom: 1px solid var(--rule);
}
.cruise-header {
  display: block; width: 100%; height: auto;
  /* Reserve the space before the image arrives, so the day tabs below
     don't jump down when it loads. Matches the 2048 x 400 artwork
     exactly -- if this and the uploaded ratio ever disagree, the
     difference shows up as a crop. */
  aspect-ratio: 2048 / 400;
  object-fit: cover; object-position: center;
}
.topbar-art .art-actions {
  position: absolute; right: 12px;
  top: calc(env(safe-area-inset-top) + 10px);
  display: flex; gap: 8px;
}
/* White icons on artwork of unknown brightness. The measured average
   behind these on the first header was mid-tone, so a plain white glyph
   would wash out over the lighter patches -- give each button its own
   dark backing rather than asking every future header to keep that
   corner clean. */
.topbar-art .icon-btn {
  background: rgba(0,0,0,.42);
  border-color: rgba(255,255,255,.34);
  color: #fff;
  backdrop-filter: saturate(140%) blur(6px);
  -webkit-backdrop-filter: saturate(140%) blur(6px);
}
.topbar-art .icon-btn svg { stroke: #fff; }
.topbar-art .icon-badge { box-shadow: 0 0 0 2px rgba(0,0,0,.42); }

@media (prefers-color-scheme: dark) {
  .topbar-art { background: var(--paper); }
}


/* An alert about your own saved event reads differently from a
   cruise-wide announcement, so it should look different too. */
.notif-alert {
  display: block; text-decoration: none; color: inherit;
  border-left: 3px solid var(--accent);
}
.notif-alert .notif-when { color: var(--ink-2); font-size: 13px; margin-top: 6px; }


/* ---------- Port-day weather ---------- */
.dayinfo-wx {
  display: inline-flex; align-items: center; gap: 4px;
  margin-left: 10px; font-size: 14px; font-weight: 600;
  color: var(--ink); white-space: nowrap;
}
.wx-icon { font-size: 16px; line-height: 1; }
.wx-low { color: var(--ink-2); font-weight: 500; }
.sr-only {
  position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
  overflow: hidden; clip: rect(0,0,0,0); white-space: nowrap; border: 0;
}


/* ============================================================
   Dark mode — follows the phone's setting.
   ============================================================
   Most of the app is built on the tokens below, so redefining them
   flips backgrounds, text, cards, dividers and the tab bar in one go.
   What follows the token block is the handful of places that used a
   literal colour and would otherwise glow like a light-mode patch. */
@media (prefers-color-scheme: dark) {
  :root {
    --paper:   #1A1C20;   /* cards, bars */
    --paper-2: #111316;   /* page ground */
    --rule:    #2C3036;
    --ink:     #ECEDEF;
    --ink-2:   #A4ABB6;
    --ink-3:   #737B87;

    /* Pure black is invisible on a dark page, and In Concert would lose
       its colour cue entirely. */
    --in-concert:  #D8DBE0;
    --information: #3A3F47;

    /* #2900F3 is a strong FILL but near-illegible as text on black.
       Buttons keep the deep blue; text-coloured uses (links, active tab,
       "more" buttons) go light grey to match the rest of the text. */
    --accent-ink:  #ECEDEF;

    /* Same washes, mixed for a dark card: a touch stronger, since a
       faint overlay disappears entirely against near-black. */
    --title-ink:     #ECEDEF;   /* dark mode: the usual off-white */
    --postponed-ink: #F3A472;
    /* Lighter than the body secondary grey. On the tinted dark cards
       this reads 7.7-9.2:1, against 5.6-6.6:1 before. */
    --meta-ink:      #C3C9D2;
    --tint-enjoy:    #07DA0024;
    --tint-concert:  #D8DBE016;
    --tint-learn:    #F3895B2A;
    --tint-info:     #8A929E14;
    --tint-red:      #FF3E3E28;
    --tint-blue:     #6E8BFF2A;
  }

  /* Links and text-style buttons that used the accent directly. */
  a, .link-btn, .bio-more, .back-btn {
    color: var(--accent-ink);
  }
  .bio-more { color: var(--accent-ink); }

  /* Selected day pill is light in dark mode: weekday matches the number. */
  .day[aria-current="true"] .d { color: inherit; }

  /* Flash banners: confirmation and error. */
  .toast {
    background: #16301C; border-bottom-color: #2A5234; color: #CFEBD6;
  }
  .toast.is-error {
    background: #3A1714; border-bottom-color: #6B2A24; color: #F6D2CE;
  }

  /* Profile saved panel. */
  .saved-note {
    background: rgba(40,190,80,.14); border-color: rgba(40,190,80,.40);
  }

  /* Account reset warning. */
  .danger-panel {
    background: rgba(255,80,80,.10); border-color: rgba(255,90,90,.40);
  }
  .danger-panel h2 { color: #FF8B85; }
  .btn-danger { background: #C0282B; }

  /* The offline notice keeps its warning orange but loses the glare. */
  .offline-bar { background: #B8612F; color: #1B1206; }

  /* Form fields: the page's colour-scheme meta handles the native bits,
     but our own styled inputs need their own surfaces. */
  input, select, textarea {
    background: #0F1114; color: var(--ink); border-color: var(--rule);
  }
  input::placeholder, textarea::placeholder { color: var(--ink-3); }

  /* Images with a white matte look like holes punched in the page. */
  .artist-hero, .card-block.artist img { background: #22252A; }

  /* Header artwork already sits on navy; the band above it follows the
     page rather than staying white. */
  .topbar-art { background: var(--paper); }

  /* Inline form errors and schedule-clash warnings — the last two light
     patches the audit turned up. */
  .error { background: rgba(255,80,80,.12); border-color: rgba(255,90,90,.35); color: #F6D2CE; }
  .clash { background: rgba(243,137,91,.14); border-color: rgba(243,137,91,.40); color: #F7D9C8; }
}


/* ============================================================
   Page transitions
   ============================================================
   Every tap is a full page load. Where the browser supports it
   (iOS 18.2+ Safari, Chrome 126+) the old page fades into the new one
   instead of blanking to white in between. Everything else ignores
   these rules and behaves exactly as before.

   The tab bar is given its own name so it holds perfectly still while
   the page above it changes -- that's most of what makes it read as an
   app rather than a website. Skipped entirely for anyone who has asked
   their phone to reduce motion. */
html { background: var(--paper-2); }   /* never a white frame between pages */

@media (prefers-reduced-motion: no-preference) {
  @view-transition { navigation: auto; }

  .nav { view-transition-name: tabbar; }

  ::view-transition-old(root),
  ::view-transition-new(root) {
    animation-duration: 160ms;
    animation-timing-function: ease-out;
  }
  /* The bar is identical on both pages: no fade, no movement. */
  ::view-transition-group(tabbar) { animation: none; }
  ::view-transition-old(tabbar) { display: none; }
  ::view-transition-new(tabbar) { animation: none; }
}


/* ============================================================
   Group chat and lounges
   ============================================================ */
.list-h {
  margin: 4px 2px 8px; font-size: 12.5px; font-weight: 650;
  text-transform: uppercase; letter-spacing: .09em; color: var(--ink-2);
}
.thread-av.group { background: var(--learn); color: #1B1206; }
.muted-tag {
  margin-left: 6px; font-size: 11px; font-weight: 600; color: var(--ink-3);
  text-transform: uppercase; letter-spacing: .06em;
}
.badge.inline.quiet { background: var(--ink-3); }
.join-pill {
  flex: 0 0 auto; padding: 5px 12px; border-radius: 999px;
  background: var(--accent); color: var(--on-accent);
  font-size: 14px; font-weight: 650;
}

.invite-card { padding: 14px 15px; }
.invite-top { display: flex; gap: 12px; align-items: center; margin-bottom: 12px; }
.invite-actions { display: flex; gap: 10px; }
.invite-actions .btn-primary, .invite-actions .btn-quiet { flex: 1 1 0; }
.btn-quiet.neutral { color: var(--ink); }
.small-link { font-size: 14px; margin-top: 8px; }

.pick { cursor: pointer; }
.pick input[type=checkbox] { width: 22px; height: 22px; flex: 0 0 auto; accent-color: var(--accent); }
.pick.is-off { opacity: .45; }
.pick[hidden] { display: none; }

.sub-link { display: block; text-decoration: none; color: var(--ink-2); }
.msg[data-gid] .bubble { cursor: pointer; }
.msg-name { font-size: 12.5px; font-weight: 650; color: var(--ink-2); margin: 0 4px 3px; }
.composer-closed {
  justify-content: center; color: var(--ink-2); font-size: 15px;
  padding: 14px 16px calc(14px + env(safe-area-inset-bottom));
}


/* ============================================================
   Add to Home Screen guide (iPhone)
   ============================================================
   Pinned to the top. Safari's menu and share sheet come up from the
   bottom, so this is the part of the screen that stays visible while
   the guest follows the steps. */
.a2hs-guide {
  position: fixed; z-index: 95; left: 10px; right: 10px;
  top: calc(env(safe-area-inset-top) + 10px);
  background: var(--paper); color: var(--ink);
  border: 2px solid var(--accent); border-radius: 14px;
  padding: 12px 14px 12px; box-shadow: 0 8px 28px rgba(0,0,0,.22);
}
.a2hs-head { display: flex; align-items: center; justify-content: space-between; gap: 10px; }
.a2hs-head strong { font-size: 17px; }
.a2hs-close {
  /* Pinned size: a global button min-height was stretching this into
     an oval and padding out the header. */
  flex: 0 0 30px; width: 30px; height: 30px; min-height: 0; min-width: 0;
  padding: 0; line-height: 30px; border-radius: 50%; border: none;
  background: var(--paper-2); color: var(--ink-2); font-size: 15px; cursor: pointer;
}
.a2hs-steps { margin: 6px 0 2px; padding-left: 22px; }
.a2hs-steps li { font-size: 16px; line-height: 1.4; margin-bottom: 5px; }
.a2hs-note { margin: 6px 0 8px; font-size: 14px; color: var(--ink-2); }
.a2hs-ic {
  display: inline-flex; align-items: center; justify-content: center;
  vertical-align: -3px; font-weight: 700; color: var(--accent-ink);
}
/* The ••• and View More buttons are grey rounded shapes on screen;
   drawn the same way here so they're recognisable at a glance. */
.a2hs-pill {
  padding: 0 7px; height: 20px; border-radius: 10px; font-size: 11px;
  letter-spacing: 1px; background: var(--paper-2); border: 1px solid var(--rule);
  color: var(--ink); vertical-align: 1px;
}
.a2hs-round {
  width: 22px; height: 22px; border-radius: 50%;
  background: var(--paper-2); border: 1px solid var(--rule); color: var(--ink);
  vertical-align: -5px;
}
.a2hs-link { color: var(--accent-ink); font-weight: 650; text-decoration: underline; }

/* Chrome and friends: sit at the bottom, over the app's tab bar, in the
   strip their top-anchored share sheet leaves uncovered. Compact. */
.a2hs-guide.at-bottom {
  top: auto; bottom: calc(env(safe-area-inset-bottom) + 8px);
  padding: 10px 12px 8px;
}
.a2hs-guide.at-bottom .a2hs-steps li { font-size: 15px; margin-bottom: 3px; }
.a2hs-guide.at-bottom .a2hs-note { margin: 2px 0 2px; }

/* Marker aimed UP at a browser button in the address bar. */
.a2hs-pointer.at-top {
  bottom: auto; top: calc(env(safe-area-inset-top) + 6px);
  flex-direction: column-reverse;
}
.a2hs-pointer.at-top svg { transform: rotate(180deg); }
.a2hs-safari { margin-top: 8px; }
.a2hs-foot .link-btn { display: block; width: 100%; margin-top: 6px; font-size: 14.5px; }
/* Room for the guide so it never sits on top of the page's own title. */
html.a2hs-on body { padding-top: 0; }

/* A nudge toward Safari's ••• button, bottom right. */
.a2hs-pointer {
  position: fixed; z-index: 95; right: 14px;
  /* Sits over the app's own tab bar, just above Safari's toolbar, so
     the arrow points straight down at the ••• button beneath it. */
  bottom: calc(env(safe-area-inset-bottom) + 6px);
  display: flex; flex-direction: column; align-items: center; gap: 2px;
  padding: 7px 11px; border-radius: 12px;
  background: var(--accent); color: var(--on-accent);
  font-size: 14px; font-weight: 700;
  box-shadow: 0 6px 18px rgba(0,0,0,.25);
  animation: a2hs-bob 1.2s ease-in-out infinite;
}
@keyframes a2hs-bob { 50% { transform: translateY(6px); } }
@media (prefers-reduced-motion: reduce) { .a2hs-pointer { animation: none; } }


/* ---------- My Schedule: unstarred in place ---------- */
.slot.unsaved .card { opacity: .5; }
.slot.unsaved .card .title { text-decoration: line-through; }
.unsaved-note { display: none; margin-top: 6px; font-size: 13px; color: var(--ink-2); }
.slot.unsaved .unsaved-note { display: block; }

/* ---------- App Suggestions ---------- */
.thread-av.suggest { background: var(--learn); color: #1B1206; }
.sugg-foot { display: flex; align-items: center; justify-content: space-between; margin-top: 10px; gap: 12px; }
.sugg-foot .btn-primary { width: auto; padding-left: 26px; padding-right: 26px; }
.sugg-item { align-items: flex-start; }
.sugg-body { display: block; white-space: pre-wrap; font-size: 15.5px; line-height: 1.45; margin-bottom: 4px; }

/* ---------- Postponed ----------
   Not the same as cancelled: it IS happening, just not when the card
   says. So the title stays readable and the TIME is what gets struck. */
.slot[data-postponed="true"] .time { text-decoration: line-through; opacity: .6; }
.tag.postponed {
  border-color: var(--postponed-ink); color: var(--postponed-ink); font-weight: 650;
}
.set-row.postponed-row .set-meta { text-decoration: line-through; opacity: .75; }
.postponed-note { margin-top: 5px; font-size: 13.5px; color: var(--postponed-ink); }


/* ============================================================
   After the cruise: schedule card, recap page, chat closing,
   My sailings.
   ============================================================ */

/* ---- Card above the day's programme ---- */
.after-cruise { padding: 12px 16px 0; }
.after-cruise[hidden] { display: none; }
.ac-card {
  position: relative; display: block;
  background: var(--paper); border: 1px solid var(--rule);
  border-left: 4px solid var(--accent);
  border-radius: 12px; padding: 14px 16px; margin-bottom: 10px;
  color: inherit; text-decoration: none;
}
.ac-title { font-size: 17.5px; font-weight: 700; padding-right: 26px; }
.ac-card p { margin: 4px 0 0; font-size: 15px; color: var(--ink-2); }
.ac-card p.ac-chat { color: var(--ink); font-weight: 600; }
.ac-card form { margin: 0; }
.ac-card .btn-primary { margin-top: 12px; }
.ac-go { display: inline-block; margin-top: 8px; font-size: 15px;
         font-weight: 650; color: var(--accent-ink); }
.ac-no { display: block; margin: 8px auto 0; font-size: 14px; }
.ac-recap { background: #011F4D; border-color: #011F4D; border-left-color: #D9A441; }
.ac-recap, .ac-recap p, .ac-recap .ac-go { color: #FFFFFF; }
.ac-recap p { opacity: .85; }
.ac-recap p.ac-chat { color: #FFFFFF; opacity: 1; }
.ac-recap .ac-go { color: #F2C66D; }
button.ac-x {
  position: absolute; top: 6px; right: 6px;
  width: 36px; min-height: 36px; margin: 0; padding: 0;
  background: none; border: none; border-radius: 50%;
  font-size: 22px; line-height: 36px; font-weight: 400;
  color: inherit; opacity: .6; cursor: pointer;
}
.ac-banner {
  position: relative; display: flex; align-items: center; gap: 8px;
  background: var(--paper); border: 1px solid var(--rule);
  border-radius: 10px; padding: 10px 44px 10px 14px; margin-bottom: 10px;
  font-size: 15px; font-weight: 600; color: inherit; text-decoration: none;
}
.ac-banner .ac-go { margin: 0; }
.ac-banner button.ac-x { top: 50%; transform: translateY(-50%); }

/* ---- Recap page ---- */
.recap-hero {
  --rc-bg: #011F4D; --rc-accent: #D9A441;
  background: var(--rc-bg);   /* for browsers without color-mix */
  background: linear-gradient(180deg, var(--rc-bg), color-mix(in srgb, var(--rc-bg) 55%, #000));
  color: #FFFFFF; text-align: center;
  border-radius: 16px; padding: 24px 18px 22px; margin-bottom: 16px;
}
.recap-logos { display: flex; justify-content: center; gap: 12px; margin-bottom: 12px; }
.recap-logos img { width: 96px; height: 96px; object-fit: contain; }
.recap-hero h2 { margin: 0; font-size: 23px; line-height: 1.2; letter-spacing: -0.01em; }
.recap-sub { margin: 6px 0 0; font-size: 14.5px; opacity: .8; }
.recap-stats {
  display: flex; justify-content: center; gap: 8px; margin: 20px 0 4px;
}
.recap-stats > div { flex: 1 1 0; max-width: 120px; }
.recap-stats b {
  display: block; font-size: 42px; line-height: 1; font-weight: 800;
  color: var(--rc-accent);
}
.recap-stats span {
  display: block; margin-top: 6px; font-size: 12px; font-weight: 650;
  text-transform: uppercase; letter-spacing: .1em; opacity: .85;
}
.recap-ports { margin: 16px 0 0; font-size: 14px; line-height: 1.5; opacity: .85; }

.recap-share { text-align: center; margin-bottom: 18px; }
.recap-card-preview {
  display: block; width: 62%; max-width: 260px; margin: 0 auto 12px;
  border-radius: 10px; box-shadow: 0 6px 22px rgba(0,0,0,.18); cursor: pointer;
}
.recap-share .btn-primary { margin-top: 0; }
.recap-share .btn-primary[disabled] { opacity: .55; }
.recap-share .note { margin-top: 8px; }
.recap-card-full { display: block; width: 100%; max-width: 320px; margin: 4px auto 12px; border-radius: 10px; }

.recap-note { font-size: 15.5px; }
.recap-note .btn-quiet, .recap-keep .btn-quiet { margin-top: 12px; }
.recap-keep { border-left: 4px solid var(--accent); }
.recap-keep-title { font-weight: 700; font-size: 17px; }
.recap-keep p { margin: 6px 0 0; font-size: 15.5px; }

.recap-artist { display: flex; gap: 13px; align-items: flex-start; }
.recap-artist img {
  width: 58px; height: 58px; border-radius: 50%; object-fit: cover;
  flex: 0 0 auto; background: var(--paper-2);
}
.recap-artist-main { min-width: 0; }
.recap-links { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 8px; }
.recap-links a {
  font-size: 13px; font-weight: 600; padding: 5px 10px;
  border: 1px solid var(--rule); border-radius: 999px;
  color: var(--accent-ink); background: var(--paper-2);
}

.recap-group {
  margin: 4px 0 8px; font-size: 13px; font-weight: 650;
  text-transform: uppercase; letter-spacing: .08em; color: var(--ink-2);
}
.recap-saved { margin-bottom: 10px; }
.recap-saved-row { cursor: default; align-items: flex-start; }
.recap-when {
  flex: 0 0 64px; font-size: 12.5px; line-height: 1.35;
  color: var(--ink-2); font-weight: 600;
}
.recap-venue { display: block; font-size: 13.5px; color: var(--ink-2); margin-top: 2px; }
.recap-more summary {
  cursor: pointer; font-size: 15px; font-weight: 600;
  color: var(--accent-ink); margin: 0 0 12px;
}

.recap-next { display: grid; grid-template-columns: 72px 1fr; gap: 6px 14px; align-items: center; }
.recap-next img { width: 72px; height: 72px; object-fit: contain; }
.recap-next .btn-primary { grid-column: 1 / -1; margin-top: 10px; }
.recap-next-name { font-weight: 700; font-size: 17px; }
.recap-next-when { font-size: 15px; }
.recap-next-where { font-size: 14px; color: var(--ink-2); }
.recap-foot { margin-top: 18px; text-align: center; font-size: 13px; }

/* ---- Chat: the final week ---- */
.chat-closing {
  margin-bottom: 16px; padding: 12px 14px; border-radius: 12px;
  background: #FFF4DE; border: 1px solid #F0C97A; color: #4A3200;
  font-size: 15px; line-height: 1.45;
}
.chat-closing strong { display: block; margin-bottom: 2px; }

/* ---- My sailings ---- */
.sailing-row { color: inherit; text-decoration: none; }
.sailing-row img { width: 40px; height: 40px; object-fit: contain; flex: 0 0 auto; }
.sailing-when { display: block; font-size: 13.5px; color: var(--ink-2); margin-top: 2px; }
.sailing-tag {
  flex: 0 0 auto; font-size: 12px; font-weight: 700; letter-spacing: .04em;
  padding: 4px 9px; border-radius: 999px;
  background: var(--accent); color: var(--on-accent);
}
.sailing-tag.now { background: var(--enjoy); color: #06260A; }
.sailing-tag.soon { background: var(--paper-2); color: var(--ink-2); border: 1px solid var(--rule); }

@media (prefers-color-scheme: dark) {
  .ac-recap { background: #0B2A5C; border-color: #1C3C72; }
  .chat-closing { background: #33260A; border-color: #6B5217; color: #F6E3B8; }
  .recap-card-preview { box-shadow: 0 6px 22px rgba(0,0,0,.5); }
}
.recap-keep-list { margin: 6px 0 0; padding-left: 20px; font-size: 15.5px; }
.recap-keep-list li { margin: 2px 0; }

/* ---- Recap share buttons ---- */
.share-row {
  display: grid; grid-template-columns: repeat(4, 1fr); gap: 8px;
  max-width: 420px; margin: 0 auto;
}
button.share-btn {
  display: flex; flex-direction: column; align-items: center; gap: 6px;
  width: auto; min-height: 0; margin: 0; padding: 10px 2px 9px;
  background: var(--paper); color: var(--ink);
  border: 1px solid var(--rule); border-radius: 12px;
  font-size: 13px; font-weight: 600; cursor: pointer;
}
button.share-btn[disabled] { opacity: .5; cursor: default; }
button.share-btn:active { background: var(--paper-2); }
.share-ic {
  display: flex; align-items: center; justify-content: center;
  width: 42px; height: 42px; border-radius: 50%; color: #FFFFFF;
}
.share-ic.dl { background: var(--ink-2); }
.share-ic.sh { background: var(--accent); }
.share-row.two { grid-template-columns: repeat(2, 1fr); max-width: 260px; }
