/* Warm light surface by default, dark for anyone whose system asks for it.
   The chrome is deliberately near-neutral so the only real colour on screen is
   the issuer's - which is the one colour that carries meaning here. */
/* Bright by default: a soft blue -> lavender -> peach wash on the page (applied
   on body), with pure-white cards lifting off it. The dark theme is preserved
   but is now opt-in through the header toggle (:root[data-theme="dark"]) rather
   than tied to the OS setting, so the site is light unless a visitor chooses
   otherwise. */
:root {
  color-scheme: light;

  --bg: #eef2ff;
  /* A soft "aurora" mesh - blue top-left, pink top-right, warm gold bottom -
     over a near-white base. Colourful but light enough that white cards still
     lift off it. */
  --page-gradient:
    radial-gradient(1100px 620px at 8% -12%, #cfe0ff 0%, transparent 55%),
    radial-gradient(1000px 680px at 104% 4%, #ffd6e7 0%, transparent 52%),
    radial-gradient(1000px 900px at 50% 118%, #ffeec2 0%, transparent 55%),
    linear-gradient(180deg, #eef2ff 0%, #fbfaff 60%, #ffffff 100%);
  --bg-elevated: #ffffff;
  --panel: #ffffff;
  --panel-soft: #eef1fb;
  --panel-hover: #e3e9f8;
  --text: #171a21;
  /* Cool-grey secondaries tuned for AA on the light surfaces they land on. */
  --muted: #4b5262;
  --subtle: #5a6273;
  --line: rgba(30, 41, 82, 0.1);
  --line-strong: rgba(59, 91, 219, 0.5);
  --accent: #3b5bdb;
  --accent-strong: #2f49b2;
  --accent-soft: rgba(59, 91, 219, 0.08);
  --accent-contrast: #ffffff;
  --good: #0e8a5f;
  --good-soft: rgba(14, 138, 95, 0.12);
  --warn: #b45309;
  --warn-soft: rgba(180, 83, 9, 0.12);
  /* Call-to-action red, used only for the "pick your cards" header button while
     nothing is selected. Kept separate from --accent (blue) so it never bleeds
     into the rest of the chrome. */
  --cta: #e5484d;
  --cta-strong: #cc3a3f;
  --cta-glow: rgba(229, 72, 77, 0.45);
  --shadow: 0 1px 2px rgba(30, 41, 82, 0.06), 0 12px 28px -14px rgba(30, 41, 82, 0.22);
  --shadow-lift: 0 2px 6px rgba(30, 41, 82, 0.08), 0 24px 48px -18px rgba(30, 41, 82, 0.3);
  --radius: 14px;
  --radius-sm: 10px;
  --radius-pill: 999px;
}

:root[data-theme="dark"] {
  color-scheme: dark;
  --bg: #14110f;
  /* The same mesh idea, dialled right down: faint blue and magenta glows over a
     warm-dark base, so night mode has a little life without glare. */
  --page-gradient:
    radial-gradient(1000px 600px at 8% -12%, rgba(84, 118, 255, 0.12) 0%, transparent 55%),
    radial-gradient(900px 680px at 104% 4%, rgba(255, 92, 150, 0.1) 0%, transparent 52%),
    linear-gradient(180deg, #16131c 0%, #14110f 100%);
  --bg-elevated: #1c1917;
  --panel: #1c1917;
  --panel-soft: #221e1b;
  --panel-hover: #2a2521;
  --text: #f5f1ec;
  --muted: #bfb8b3;
  --subtle: #b2aba5;
  --line: rgba(245, 241, 236, 0.12);
  --line-strong: rgba(147, 168, 255, 0.5);
  --accent: #93a8ff;
  --accent-strong: #b3c2ff;
  --accent-soft: rgba(147, 168, 255, 0.14);
  --accent-contrast: #14110f;
  --good: #4ade80;
  --good-soft: rgba(74, 222, 128, 0.14);
  --warn: #fbbf24;
  --warn-soft: rgba(251, 191, 36, 0.14);
  --cta: #f0575c;
  --cta-strong: #ff7b80;
  --cta-glow: rgba(240, 87, 92, 0.4);
  --shadow: 0 1px 2px rgba(0, 0, 0, 0.4), 0 12px 28px -12px rgba(0, 0, 0, 0.6);
  --shadow-lift: 0 2px 6px rgba(0, 0, 0, 0.45), 0 22px 44px -16px rgba(0, 0, 0, 0.7);
}

/* Issuer colours. Set here rather than on the pill so a promotion card and its
   card pills can both read --bank. See the sourcing notes further down. */
.bank-kbank       { --bank: #1da858; --bank-ink: #10693a; }
.bank-bbl         { --bank: #29449d; }
.bank-krungsri    { --bank: #d9a400; --bank-ink: #7a5c00; }
.bank-ttb         { --bank: #0c55f2; }
.bank-uob         { --bank: #e41a26; }
.bank-amex        { --bank: #006fcf; }
.bank-ktc         { --bank: #b42217; }
.bank-aeon        { --bank: #7461cf; }
.bank-cardx       { --bank: #543186; }
.bank-centralthe1 { --bank: #e4002b; }
.bank-firstchoice { --bank: #f5821f; --bank-ink: #9a4a00; }

* {
  box-sizing: border-box;
}

body {
  margin: 0;
  min-width: 320px;
  /* Solid fallback first, then the themed wash. Fixed so the colour stays put
     while the promotion list scrolls over it. */
  background: var(--bg);
  background-image: var(--page-gradient);
  background-attachment: fixed;
  color: var(--text);
  /* system-ui leads so Thai and Latin come from the same family where possible;
     Thai faces follow for platforms whose default lacks good Thai. */
  font-family: system-ui, -apple-system, "Segoe UI", "Noto Sans Thai",
    "IBM Plex Sans Thai", Thonburi, Inter, sans-serif;
  /* Thai stacks tone marks above and below the line, so it needs more room than
     Latin to avoid looking cramped. */
  line-height: 1.65;
  -webkit-font-smoothing: antialiased;
}

a {
  color: inherit;
}

.app-shell {
  min-height: 100vh;
}

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

.app-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 18px;
  padding: 14px 30px;
  background: var(--bg-elevated);
  border-bottom: 1px solid var(--line);
  position: sticky;
  top: 0;
  z-index: 20;
}

.app-header .brand-block {
  margin-bottom: 0;
}

.header-actions {
  display: flex;
  align-items: center;
  gap: 12px;
}

.account-chip {
  color: var(--muted);
  font-size: 13px;
}

.header-button {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  min-height: 38px;
  padding: 0 16px;
  border: 1px solid var(--line);
  border-radius: var(--radius-pill);
  background: var(--panel-soft);
  color: var(--text);
  font: inherit;
  font-size: 13.5px;
  font-weight: 600;
  cursor: pointer;
  transition: background 0.15s ease, border-color 0.15s ease;
}

.header-button:hover {
  border-color: var(--line-strong);
  background: var(--panel-hover);
}

/* ---------- settings dialog ---------- */

.settings-dialog {
  width: min(760px, calc(100vw - 32px));
  max-height: min(88vh, 900px);
  padding: 0;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--bg-elevated);
  color: var(--text);
  box-shadow: var(--shadow-lift);
  grid-template-rows: auto minmax(0, 1fr) auto;
  overflow: hidden;
}

/* The `display` belongs on [open] and nowhere else. The browser hides a closed
   dialog with `dialog:not([open]) { display: none }` in its own stylesheet, and
   any author `display` at all beats that whatever its specificity, because
   author styles outrank the user agent's. A bare `display: grid` here left both
   dialogs laid out in normal flow at the foot of the page, below 400-odd
   promotion cards, where you only meet them by scrolling to the bottom.
   The body scrolls, not the dialog, so the header and Done stay put. */
.settings-dialog[open] {
  display: grid;
}

.settings-dialog::backdrop {
  background: rgba(28, 25, 23, 0.45);
  backdrop-filter: blur(2px);
}

.settings-header {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 14px;
  padding: 20px 22px 14px;
  border-bottom: 1px solid var(--line);
}

.settings-header h2 {
  margin: 2px 0 0;
  font-size: 20px;
  font-weight: 700;
  letter-spacing: -0.01em;
}

.settings-body {
  overflow-y: auto;
  padding: 18px 22px 22px;
}

.settings-section + .settings-section {
  margin-top: 26px;
  padding-top: 22px;
  border-top: 1px solid var(--line);
}

.settings-heading {
  margin: 0 0 10px;
  font-size: 13px;
  font-weight: 700;
  color: var(--muted);
}

.settings-footer {
  display: flex;
  justify-content: flex-end;
  padding: 14px 22px;
  border-top: 1px solid var(--line);
  background: var(--panel-soft);
}

/* Until a card is chosen the header button is the page's one call to action, so
   it is pulled out of the quiet chrome into a solid, warm accent that reads as
   "start here". A soft pulse draws the eye without nagging; it respects
   reduced-motion. Once a card is picked the class is dropped and it returns to
   the neutral header style above. */
.header-button.needs-cards {
  background: var(--cta);
  border-color: var(--cta);
  color: #fff;
  font-weight: 700;
  box-shadow: 0 2px 10px -2px var(--cta-glow);
  animation: cta-pulse 2.4s ease-in-out infinite;
}

.header-button.needs-cards:hover {
  background: var(--cta-strong);
  border-color: var(--cta-strong);
}

@keyframes cta-pulse {
  0%, 100% { box-shadow: 0 2px 10px -2px var(--cta-glow); }
  50% { box-shadow: 0 2px 18px 1px var(--cta-glow); }
}

@media (prefers-reduced-motion: reduce) {
  .header-button.needs-cards { animation: none; }
}

.brand-block {
  display: flex;
  align-items: center;
  gap: 12px;
  margin-bottom: 26px;
}

.brand-block h1 {
  margin: 0;
  font-size: 19px;
  font-weight: 700;
  letter-spacing: -0.01em;
}

.brand-block p {
  margin: 0;
  color: var(--muted);
  font-size: 13px;
}

.brand-mark {
  display: grid;
  place-items: center;
  width: 40px;
  height: 40px;
  border-radius: 12px;
  background: linear-gradient(135deg, var(--accent), var(--accent-strong));
  color: #fff;
  font-weight: 700;
  font-size: 15px;
  flex: none;
}

.field-label {
  display: block;
  color: var(--muted);
  font-size: 12px;
  font-weight: 600;
  /* Small caps everywhere was a big part of what made this feel like a console,
     so labels keep their normal casing and lean on weight instead. */
  margin-bottom: 6px;
}

.input {
  width: 100%;
  min-height: 42px;
  padding: 9px 13px;
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  background: var(--panel-soft);
  color: var(--text);
  font: inherit;
  transition: border-color 0.15s ease, background 0.15s ease;
}

.input::placeholder {
  color: var(--subtle);
}

.input:focus {
  outline: none;
  border-color: var(--line-strong);
  background: var(--panel);
  box-shadow: 0 0 0 3px var(--accent-soft);
}

.card-list {
  display: grid;
  gap: 4px;
  margin-top: 14px;
  /* 146 checkboxes would push Done off screen; the list scrolls within itself. */
  max-height: 440px;
  overflow-y: auto;
  padding-right: 4px;
}

/* Search field and its Clear button share one row - Clear sits next to the box
   it clears rather than stranded on a half-width line of its own. */
.card-search-row {
  display: flex;
  gap: 8px;
  align-items: stretch;
}

.card-search-row .input {
  flex: 1 1 auto;
  min-width: 0;
}

.card-search-row .sidebar-action {
  flex: 0 0 auto;
  white-space: nowrap;
  padding: 0 20px;
}

.sidebar-action {
  min-height: 38px;
  padding: 0 12px;
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  /* Sits on --bg-elevated in the dialog, so --panel would vanish. */
  background: var(--panel-soft);
  color: var(--text);
  font: inherit;
  font-weight: 600;
  font-size: 13px;
  cursor: pointer;
  transition: background 0.15s ease, border-color 0.15s ease;
}

.sidebar-action:hover {
  border-color: var(--line-strong);
  background: var(--panel-hover);
}

/* ---------- account ---------- */

.auth-panel {
  display: grid;
  gap: 9px;
  margin: 4px 0 22px;
  padding: 14px;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--panel-soft);
}

.auth-signed-out,
.auth-signed-in {
  display: grid;
  gap: 9px;
}

/* `hidden` only sets display:none in the UA stylesheet, which the rule above
   outranks. Without this both auth states render at once. */
.auth-signed-out[hidden],
.auth-signed-in[hidden] {
  display: none;
}

.auth-note {
  margin: 0;
  color: var(--muted);
  font-size: 12.5px;
  line-height: 1.5;
}

.auth-panel .sidebar-actions {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 8px;
}

.auth-error {
  margin: 0;
  color: var(--warn);
  font-size: 12.5px;
}

/* ---------- card picker: grouped by bank ---------- */

.bank-group {
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  background: var(--panel-soft);
  overflow: hidden;
}

.bank-group[open] {
  padding-bottom: 6px;
}

.bank-summary {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 12px 14px;
  cursor: pointer;
  user-select: none;
  font-size: 14px;
  font-weight: 700;
  /* Remove the default disclosure triangle; a chevron is drawn below instead. */
  list-style: none;
}

.bank-summary::-webkit-details-marker {
  display: none;
}

.bank-summary:hover {
  background: var(--panel-hover);
}

.bank-summary:focus-visible {
  outline: 2px solid var(--line-strong);
  outline-offset: -2px;
}

.bank-name {
  flex: 1 1 auto;
  min-width: 0;
}

.bank-count {
  flex: 0 0 auto;
  font-size: 12px;
  font-weight: 600;
  color: var(--subtle);
}

.bank-count.has-selected {
  color: var(--accent);
}

/* A CSS chevron that rotates from pointing down (closed) to up (open). */
.bank-summary::after {
  content: "";
  flex: 0 0 auto;
  width: 8px;
  height: 8px;
  border-right: 2px solid var(--muted);
  border-bottom: 2px solid var(--muted);
  transform: rotate(45deg);
  transition: transform 0.15s ease;
}

.bank-group[open] .bank-summary::after {
  transform: rotate(-135deg);
}

.bank-group .card-option {
  margin: 2px 8px;
}

@media (prefers-reduced-motion: reduce) {
  .bank-summary::after {
    transition: none;
  }
}

.card-option {
  display: grid;
  grid-template-columns: 18px minmax(0, 1fr);
  align-items: start;
  gap: 10px;
  padding: 9px 11px;
  border: 1px solid transparent;
  border-radius: var(--radius-sm);
  font-size: 13.5px;
  line-height: 1.45;
  cursor: pointer;
  transition: background 0.12s ease;
}

.card-option:hover {
  background: var(--panel-hover);
}

.card-option input {
  margin: 3px 0 0;
  accent-color: var(--accent);
  cursor: pointer;
}

.card-option.active {
  border-color: color-mix(in srgb, var(--accent) 35%, transparent);
  background: var(--accent-soft);
  font-weight: 600;
}

/* ---------- main ---------- */

.main {
  /* Beyond this the eye has to travel too far across a card, and three columns
     stop fitting cleanly. Wider screens get margin rather than wider cards. */
  max-width: 1440px;
  margin-inline: auto;
  padding: 22px 30px 60px;
  min-width: 0;
}

.topbar {
  display: flex;
  align-items: flex-end;
  justify-content: space-between;
  gap: 18px;
  margin-bottom: 22px;
}

.topbar h2 {
  margin: 2px 0 0;
  font-size: 26px;
  font-weight: 700;
  letter-spacing: -0.02em;
  line-height: 1.3;
}

.eyebrow {
  margin: 0;
  color: var(--muted);
  font-size: 13px;
}


/* ---------- metrics ---------- */

/* These are a running total, not the point of the page, so they sit on one
   compact line instead of four panels the width of the viewport. */
.metrics {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 6px 22px;
  margin-bottom: 16px;
  padding: 10px 16px;
  border: 1px solid var(--line);
  border-radius: var(--radius-pill);
  background: var(--panel);
}

.metric {
  display: flex;
  align-items: baseline;
  gap: 7px;
}

.metric span {
  font-size: 17px;
  font-weight: 700;
  letter-spacing: -0.01em;
}

.metric label {
  color: var(--muted);
  font-size: 12.5px;
}

/* ---------- filters ---------- */

.toolbar {
  display: grid;
  grid-template-columns: minmax(260px, 1fr) 190px 210px 160px;
  gap: 12px;
  padding: 16px;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--panel);
  box-shadow: var(--shadow);
  margin-bottom: 20px;
}

.result-header {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 14px;
  margin-bottom: 14px;
  padding: 0 2px;
}

.result-header h3 {
  margin: 0;
  font-size: 17px;
  font-weight: 700;
  letter-spacing: -0.01em;
}

.result-header p {
  margin: 0;
  color: var(--subtle);
  font-size: 12.5px;
}

/* ---------- promotion cards ---------- */

.promotion-list {
  display: grid;
  /* Three across at this container width; fewer as it narrows, with no
     breakpoint to maintain. */
  grid-template-columns: repeat(auto-fill, minmax(380px, 1fr));
  gap: 14px;
  align-items: stretch;
}

.promo-card {
  position: relative;
  /* Vertical now: image on top, content filling the rest, so equal-height grid
     cells still line their buttons up along the bottom. */
  display: flex;
  flex-direction: column;
  gap: 13px;
  padding: 14px;
  padding-left: 18px;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--panel);
  box-shadow: var(--shadow);
  overflow: hidden;
  transition: box-shadow 0.18s ease, transform 0.18s ease;
}

/* A quiet stripe of the issuer's colour, so the bank registers before any text
   is read. Full-bleed via a pseudo-element rather than border-left, which would
   fight the card's radius. */
.promo-card::before {
  content: "";
  position: absolute;
  inset: 0 auto 0 0;
  width: 4px;
  background: var(--bank, var(--accent));
}

.promo-card:hover {
  box-shadow: var(--shadow-lift);
  transform: translateY(-1px);
}

/* The card is a column now, so a missing image needs nothing; the drawer still
   places its image beside the summary and does. */
.drawer-hero.no-image {
  grid-template-columns: minmax(0, 1fr);
}

/* The source artwork comes in two shapes - 700x440 and 726x521 - so any single
   crop cut one of them. The frame is a fixed height for a steady grid rhythm,
   the image is shown whole inside it, and a blurred copy of the same artwork
   fills the remainder rather than leaving empty bars. */
.promo-media {
  position: relative;
  height: 172px;
  border-radius: var(--radius-sm);
  overflow: hidden;
  background: var(--panel-soft);
  flex: none;
}

.promo-media::before {
  content: "";
  position: absolute;
  /* Overscanned so the blur has no soft edge against the frame. */
  inset: -24px;
  background-image: var(--art);
  background-size: cover;
  background-position: center;
  filter: blur(18px) saturate(1.3);
  opacity: 0.5;
}

.promo-image {
  position: relative;
  width: 100%;
  height: 100%;
  object-fit: contain;
}

.promo-content {
  min-width: 0;
  display: flex;
  flex-direction: column;
  flex: 1;
}

.promo-title-row {
  display: block;
}

/* Two lines, always. Titles ran from one line to three, and because everything
   below is stacked, that alone put the facts strip and the summary of three
   cards in a row at three different heights. A fixed band costs a little white
   space under short titles and buys a grid that lines up; the full title is on
   the tooltip and in the drawer. */
.promo-title {
  margin: 0;
  font-size: 16.5px;
  font-weight: 700;
  line-height: 1.45;
  letter-spacing: -0.01em;
  min-height: 2.9em;
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
}

/* One line, always, for the same reason - and a fixed height, because a card
   carrying a status pill would otherwise stand 6px taller here than the card
   beside it and push everything below it out of line. */
.promo-meta {
  display: flex;
  align-items: center;
  gap: 8px;
  margin-top: 9px;
  /* A pill measures 27px here and the bare text line 25px, so a card with a
     status pill stood 3px taller than the one beside it. Both are centred in a
     band sized to hold the taller of the two. */
  min-height: 28px;
  min-width: 0;
  font-size: 13px;
}

.promo-facets {
  min-width: 0;
  color: var(--muted);
  font-size: 12.5px;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.promo-meta .pill {
  flex: none;
}

.pill {
  display: inline-flex;
  align-items: center;
  min-height: 25px;
  border-radius: var(--radius-pill);
  border: 1px solid var(--line);
  background: var(--panel-soft);
  color: var(--muted);
  padding: 3px 10px;
  font-size: 12px;
  font-weight: 600;
}

/* Amber-on-amber-tint is 4.26:1 at the raw token, just under AA for text this
   small. Pulled 15% toward the text colour it clears in both themes - 5.20
   light, 8.15 dark - and still reads as the same amber as the block below. */
.pill.register {
  border-color: color-mix(in srgb, var(--warn) 32%, transparent);
  background: var(--warn-soft);
  color: color-mix(in srgb, var(--warn) 85%, var(--text));
}

.pill.clear {
  border-color: color-mix(in srgb, var(--good) 32%, transparent);
  background: var(--good-soft);
  color: var(--good);
}

/* Not a warning - a points cost is a fact about the offer, not a hazard - so it
   takes the accent rather than the amber the registration pill uses. */
.pill.points {
  border-color: color-mix(in srgb, var(--accent) 32%, transparent);
  background: var(--accent-soft);
  color: color-mix(in srgb, var(--accent) 78%, var(--text));
}

.pill.unknown {
  border-color: var(--line);
  background: var(--panel-soft);
  color: var(--subtle);
}

/* Three stacked label/value rows cost roughly a hundred pixels on every card to
   carry two numbers and a status the pill above already gave. Both numbers now
   share one line: the value keeps its emphasis on the left, the minimum sits
   quietly at the right edge, and each wraps to its own line only where the card
   is too narrow to hold them. */
.promo-facts {
  display: flex;
  align-items: baseline;
  flex-wrap: wrap;
  gap: 3px 14px;
  margin-top: 12px;
  padding: 7px 12px;
  border-radius: var(--radius-sm);
  background: var(--panel-soft);
}

.fact {
  display: flex;
  align-items: baseline;
  gap: 7px;
  min-width: 0;
}

.fact label {
  color: var(--subtle);
  font-size: 12px;
  font-weight: 600;
  line-height: 1.5;
  flex: none;
}

.fact strong {
  font-size: 13.5px;
  font-weight: 700;
  line-height: 1.5;
  min-width: 0;
}

/* The headline number is the thing people are actually shopping for; the auto
   margin pushes the minimum to the far edge, so the two never read as one
   phrase. */
.fact-value {
  margin-right: auto;
}

.fact-value strong {
  /* Pulled a little toward the text colour so every issuer clears AA on this
     tint, including any added later, rather than four hand-tuned exceptions. */
  color: color-mix(in srgb, var(--bank-ink, var(--bank, var(--accent))) 82%, var(--text));
  font-size: 16px;
}

/* Sits directly above the buttons: the last thing read before acting is what to
   do first. Amber ties it to the "Register first" pill at the top of the card. */
.registration-preview {
  margin-top: 12px;
  padding: 9px 12px;
  border-radius: var(--radius-sm);
  background: var(--warn-soft);
  border-left: 3px solid color-mix(in srgb, var(--warn) 55%, transparent);
}

.registration-preview .field-label {
  margin-bottom: 2px;
  color: color-mix(in srgb, var(--warn) 85%, var(--text));
}

.registration-preview p {
  margin: 0;
  font-size: 13px;
  line-height: 1.55;
  /* An SMS format can run to five lines. Clamped so the longest one cannot set
     the height of every card in its row; the drawer shows it in full. */
  display: -webkit-box;
  -webkit-line-clamp: 3;
  -webkit-box-orient: vertical;
  overflow: hidden;
}

.eligible-selected-cards {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
  margin-top: 12px;
  /* Holds one pill row even when nothing is selected, so this band does not
     collapse on some cards and shift everything under it out of line. */
  min-height: 25px;
}

.card-pill-more {
  color: var(--subtle);
  cursor: default;
}

/* Card pills take the issuer's colour. Only --bank varies; the border, fill and
   text are derived from it, so a new bank is one line and cannot produce an
   unreadable combination by accident.

   Verified from each issuer's own site or brand assets: Amex #006FCF appears
   206 times in their own CSS, KTC #B42217 in theirs, AEON #7461CF in theirs.
   KBank, Bangkok Bank, Krungsri, ttb and UOB come from published brand assets;
   CardX uses SCB's purple, being its parent. Central The 1 and First Choice are
   judgment calls - both sites ship an unmodified Bootstrap palette, so scraping
   them yields framework defaults rather than brand colours.

   Krungsri's #FFD51C is darkened to #D9A400 here: the true yellow cannot reach
   4.5:1 on a light background at any sensible tint. */
.selected-card-pill {
  border-color: color-mix(in srgb, var(--bank, var(--accent)) 30%, transparent);
  background: color-mix(in srgb, var(--bank, var(--accent)) 10%, transparent);
  color: color-mix(in srgb, var(--bank-ink, var(--bank, var(--accent))) 88%, var(--text));
}

:root[data-theme="dark"] .selected-card-pill {
  border-color: color-mix(in srgb, var(--bank, var(--accent)) 42%, transparent);
  background: color-mix(in srgb, var(--bank, var(--accent)) 18%, transparent);
  color: color-mix(in srgb, var(--bank, var(--accent)) 30%, #eef6ff);
}

/* No `.selected` override here on purpose. In the drawer's card list, green
   already means "one of yours" against the other eligible cards, and that
   distinction is more useful there than the issuer's colour. */

/* Every card in a grid row is as tall as the tallest, and the slack used to
   collect as a dead gap above the buttons - worst on the cards with no
   registration block to carry. The summary takes that space instead, so a card
   with room to spare spends it on the one thing here that differs per
   promotion. Two lines on a crowded card, six on a roomy one.

   The fade is what makes a variable cut look deliberate, and it is applied only
   when text is genuinely being cut off - `is-clipped` comes from a
   ResizeObserver in app.js, because whether this region overflows is not known
   until the grid has stretched every card in the row. Fading unconditionally
   dimmed the last line whether or not anything was hidden behind it. */
.terms-preview {
  flex: 1 1 auto;
  min-height: 0;
  /* Grows into spare space, but only to here. Without a ceiling the region has
     no upper bound at all now that the two-line clamp is gone, and one
     promotion listing sixty participating branches stretched its card to 972px
     and every card in its row with it. Roughly seven lines plus the label - a
     touch more than before, so content that used to overflow by a few pixels
     (and get needlessly faded) now fits in full. */
  max-height: 180px;
  margin-top: 12px;
  overflow: hidden;
}

.terms-preview.is-clipped {
  -webkit-mask-image: linear-gradient(to bottom, #000 calc(100% - 18px), transparent);
  mask-image: linear-gradient(to bottom, #000 calc(100% - 18px), transparent);
}

.terms-preview .field-label {
  margin-bottom: 3px;
}

.terms-preview .terms {
  margin: 0;
}

.applies-label {
  margin-top: 10px;
  margin-bottom: 3px;
}

.terms {
  color: var(--muted);
  font-size: 13.2px;
  line-height: 1.6;
}

.promo-actions {
  display: flex;
  gap: 8px;
  flex-wrap: wrap;
  margin-top: auto;
  padding-top: 14px;
}

/* Equal widths, and they wrap rather than squeeze when a third appears. */
.promo-actions .button-link {
  flex: 1 1 30%;
  justify-content: center;
}

/* The same amber as the registration pill and the how-to block, so the three
   read as one instruction rather than three unrelated marks. Mixed 85% toward
   --text for the same reason the pill is: the raw token is 4.26:1 here. */
.promo-actions .button-link.register-link {
  border-color: color-mix(in srgb, var(--warn) 45%, transparent);
  background: var(--warn-soft);
  color: color-mix(in srgb, var(--warn) 85%, var(--text));
}

.promo-actions .button-link.register-link:hover {
  border-color: color-mix(in srgb, var(--warn) 70%, transparent);
  background: color-mix(in srgb, var(--warn) 22%, var(--panel));
}

.button-link {
  display: inline-flex;
  align-items: center;
  min-height: 36px;
  padding: 0 16px;
  border: 1px solid var(--line);
  border-radius: var(--radius-pill);
  background: var(--panel);
  color: var(--text);
  font: inherit;
  font-size: 13.5px;
  font-weight: 600;
  text-decoration: none;
  cursor: pointer;
  transition: background 0.15s ease, border-color 0.15s ease;
}

.button-link:hover {
  border-color: var(--line-strong);
  background: var(--panel-hover);
}

.button-link.primary {
  border-color: transparent;
  background: var(--accent);
  color: var(--accent-contrast);
}

.button-link.primary:hover {
  background: var(--accent-strong);
}

/* The app's accent is a flat periwinkle that belonged to nothing on the card it
   sat under. A promotion already has a colour - its issuer's, on the stripe and
   the card pills - so the primary action borrows it and the card reads as one
   object. This is the tint the card pills use, which is measured, with a
   stronger border and fill so it still reads as a button rather than a chip.
   The accent stays for app chrome, where there is no issuer to speak for.

   Anything filled solid with --bank instead would have to survive Krungsri's
   yellow and First Choice's orange, which is what --bank-ink exists for and why
   this stays a tint. Sweep every issuer in both themes before changing it. */
.promo-actions .button-link.primary,
.drawer-actions .button-link.primary {
  border-color: color-mix(in srgb, var(--bank, var(--accent)) 45%, transparent);
  background: color-mix(in srgb, var(--bank, var(--accent)) 14%, var(--panel));
  /* 88% is what the card pills use, and against this slightly stronger fill it
     left UOB at 4.48 and Central The 1 at 4.52. 78% clears every issuer. */
  color: color-mix(in srgb, var(--bank-ink, var(--bank, var(--accent))) 78%, var(--text));
}

.promo-actions .button-link.primary:hover,
.drawer-actions .button-link.primary:hover {
  border-color: color-mix(in srgb, var(--bank, var(--accent)) 65%, transparent);
  background: color-mix(in srgb, var(--bank, var(--accent)) 22%, var(--panel));
}

:root[data-theme="dark"] .promo-actions .button-link.primary,
:root[data-theme="dark"] .drawer-actions .button-link.primary {
  background: color-mix(in srgb, var(--bank, var(--accent)) 22%, var(--panel));
  color: color-mix(in srgb, var(--bank, var(--accent)) 26%, #eef6ff);
}

:root[data-theme="dark"] .promo-actions .button-link.primary:hover,
:root[data-theme="dark"] .drawer-actions .button-link.primary:hover {
  background: color-mix(in srgb, var(--bank, var(--accent)) 32%, var(--panel));
}

.empty-state {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 10px;
  padding: 44px 20px;
  border: 1px dashed var(--line);
  border-radius: var(--radius);
  text-align: center;
  color: var(--muted);
}

/* An author `display` beats the `hidden` attribute's UA `display:none`, so
   without this the empty state stays on screen behind the results once the app
   sets emptyState.hidden = true. */
.empty-state[hidden] {
  display: none;
}

.empty-state p {
  margin: 0;
  max-width: 42ch;
}

.empty-state-title {
  font-size: 16px;
  font-weight: 700;
  color: var(--text);
}

.empty-state .button-link {
  margin-top: 6px;
}

/* The first-run call to action, in the middle of the empty feed. It borrows the
   header button's CTA red so the reader reads the two as the same action. */
.empty-state-cta {
  margin-top: 10px;
  min-height: 46px;
  padding: 0 30px;
  border: 1px solid var(--cta);
  border-radius: var(--radius-pill);
  background: var(--cta);
  color: #fff;
  font: inherit;
  font-size: 15px;
  font-weight: 700;
  cursor: pointer;
  box-shadow: 0 2px 14px -2px var(--cta-glow);
  transition: background 0.15s ease, border-color 0.15s ease, transform 0.1s ease;
}

.empty-state-cta:hover {
  background: var(--cta-strong);
  border-color: var(--cta-strong);
}

.empty-state-cta:active {
  transform: translateY(1px);
}

/* ---------- detail dialog ---------- */

/* Centred, and deliberately the same shape as the settings dialog above: one
   modal pattern in the app rather than a panel that slid in from the edge for
   this one case. The browser supplies the backdrop, Escape and the focus trap. */
.detail-dialog {
  /* This holds full bank terms - 4 KB on average, 38 KB at worst - so it takes
     the room a desktop has rather than the 680px a side panel used to be able
     to spare. */
  width: min(1040px, calc(100vw - 48px));
  max-height: min(90vh, 1040px);
  padding: 0;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--bg-elevated);
  color: var(--text);
  box-shadow: var(--shadow-lift);
  grid-template-rows: auto minmax(0, 1fr);
  overflow: hidden;
}

/* On [open] only - see the note on .settings-dialog[open]. */
.detail-dialog[open] {
  display: grid;
}

.detail-dialog::backdrop {
  background: rgba(28, 25, 23, 0.45);
  backdrop-filter: blur(2px);
}

.drawer-header {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 14px;
  padding: 20px 22px 14px;
  border-bottom: 1px solid var(--line);
}

.drawer-header h3 {
  margin: 3px 0 0;
  font-size: 19px;
  font-weight: 700;
  line-height: 1.4;
  letter-spacing: -0.01em;
}

.icon-button {
  width: 34px;
  height: 34px;
  flex: none;
  border: 1px solid var(--line);
  border-radius: 50%;
  background: var(--panel);
  color: var(--muted);
  font-size: 17px;
  line-height: 1;
  cursor: pointer;
}

/* The night/day toggle reuses the round icon button; a little emoji sits inside,
   so the font-size is nudged down and a hover is added to read as interactive. */
.theme-toggle {
  font-size: 15px;
  transition: border-color 0.15s ease, background 0.15s ease;
}

.theme-toggle:hover {
  border-color: var(--line-strong);
  background: var(--panel-hover);
}

.icon-button:hover {
  border-color: var(--line-strong);
  color: var(--text);
}

.drawer-body {
  overflow-y: auto;
  padding: 22px 26px 40px;
}

.drawer-hero {
  display: grid;
  grid-template-columns: 340px minmax(0, 1fr);
  gap: 22px;
  align-items: start;
}

.drawer-hero img {
  width: 100%;
  aspect-ratio: 12 / 7;
  /* Whole image here too; the dialog is where someone looks closely. */
  object-fit: contain;
  border-radius: var(--radius-sm);
  background: var(--panel-soft);
}

.drawer-hero-main {
  display: flex;
  flex-direction: column;
  gap: 18px;
  min-width: 0;
}

/* Two by two. One column of four facts across a 1,040px dialog left each label
   marooned at one end of its own line. */
.drawer-summary {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 10px 24px;
  align-content: start;
}

.drawer-fact {
  padding-bottom: 9px;
  border-bottom: 1px solid var(--line);
}

.drawer-fact:last-child {
  padding-bottom: 0;
  border-bottom: 0;
}

.drawer-fact label {
  display: block;
  color: var(--subtle);
  font-size: 12px;
  font-weight: 600;
}

.drawer-fact strong {
  font-size: 14.5px;
  font-weight: 700;
}

.drawer-section {
  margin-top: 22px;
}

.drawer-section h4 {
  margin: 0 0 6px;
  font-size: 12.5px;
  font-weight: 700;
  color: var(--muted);
}

.drawer-section p {
  margin: 0;
  font-size: 14px;
  line-height: 1.7;
  white-space: pre-line;
}

.drawer-card-list {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
}

/* The drawer lists every card a promotion accepts, not just yours, so the two
   need telling apart. Green did that but replaced the issuer's colour, and when
   you hold all of them the whole list turned green and matched nothing on the
   card that opened it.

   Yours now keep the issuer's colour - identical to the promotion card - and
   the ones you do not hold recede to neutral. Same distinction, no mismatch. */
.drawer-card-list .selected-card-pill:not(.selected) {
  border-color: var(--line);
  background: transparent;
  color: var(--subtle);
}

.drawer-card-list .selected {
  border-color: color-mix(in srgb, var(--bank, var(--accent)) 45%, transparent);
  background: color-mix(in srgb, var(--bank, var(--accent)) 16%, transparent);
  color: color-mix(in srgb, var(--bank-ink, var(--bank, var(--accent))) 88%, var(--text));
}

/* No margin: it sits in the hero column now, where the gap spaces it. */
/* Two columns: what you spend, what you get. A table rather than prose because
   the reader is scanning for their own number, not reading a sentence. */
.tier-table {
  display: grid;
  grid-template-columns: auto 1fr;
  gap: 8px 20px;
  align-items: baseline;
  margin-top: 4px;
}

.tier-spend {
  color: var(--muted);
  font-size: 13.2px;
}

.tier-reward {
  font-size: 13.5px;
  font-weight: 700;
  color: color-mix(in srgb, var(--bank-ink, var(--bank, var(--accent))) 78%, var(--text));
}

.drawer-actions {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
}

/* ---------- responsive ---------- */

@media (max-width: 980px) {
  .card-list {
    grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
  }

  .toolbar {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }
}

@media (max-width: 680px) {
  .main {
    padding: 18px;
  }

  .app-header {
    padding: 12px 18px;
  }

  .topbar,
  .result-header,
  .promo-title-row {
    display: block;
  }

  .topbar h2 {
    font-size: 22px;
  }

  .toolbar,
  .drawer-hero,
  .drawer-summary {
    grid-template-columns: 1fr;
  }

  .detail-dialog {
    width: calc(100vw - 16px);
    max-height: 92vh;
  }
}

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