/* =========================================================
   OFC Poker & Blackjack21 — Marketing Website
   Design system: dark casino-felt theme (green + gold + red)
   ========================================================= */

:root {
  /* --- Brand palette: cinematic noir/heist theme, matched to official key art --- */
  --color-bg: #0a0a0d;
  --color-bg-alt: #131217;
  --color-surface: #19161c;
  --color-surface-2: #221d24;
  --color-border: rgba(212, 175, 55, 0.16);

  --color-primary: #7a1626;
  --color-primary-light: #a52236;
  --color-gold: #d4af37;
  --color-gold-light: #f2d879;
  --color-red: #b3272d;
  --color-red-light: #e0574a;

  --color-text: #f5f1e6;
  --color-text-muted: #aebdb4;
  --color-text-faint: #7c8c84;

  --radius-sm: 8px;
  --radius: 16px;
  --radius-lg: 28px;

  --shadow-card: 0 12px 32px rgba(0, 0, 0, 0.35);
  --shadow-glow: 0 0 0 1px rgba(212, 175, 55, 0.25), 0 20px 45px rgba(0, 0, 0, 0.45);

  --font-heading: 'Poppins', 'Noto Sans TC', sans-serif;
  --font-body: 'Noto Sans TC', 'Poppins', sans-serif;

  --max-width: 1160px;

  /* --- Type scale: every paragraph/label/tag size on the site should trace
     back to one of these steps, so text weight reads as a deliberate
     hierarchy (label → caption → body → lead → statement) instead of
     scattered one-off rem values. --- */
  --fs-xs: 0.8rem;    /* small uppercase labels, tags, badges */
  --fs-sm: 0.92rem;   /* captions, card copy, secondary supporting text */
  --fs-base: 1rem;    /* standard paragraph copy */
  --fs-md: 1.08rem;   /* lead sentence / opening line of a copy block */
  --fs-lg: 1.15rem;   /* emphasized closing statement, pull-quote */
}

* { box-sizing: border-box; }
/* v24: background-color added here (was only on body) so the very first
   paint of a fresh navigation — before body's own gradient background has
   anything to render against — is already dark instead of the browser's
   default white. This plus <meta name="color-scheme" content="dark"> in
   every page's <head> (tells the browser itself to default to a dark UI/
   background before any CSS or content has painted at all) together fix
   the "flash of white" the user saw between full-page navigations on this
   otherwise all-dark site — see how-to-play-page-task-context.md "v24". */
html { scroll-behavior: smooth; background: var(--color-bg); }
/* The UA's [hidden]{display:none} only targets HTML elements, so an <svg
   hidden> still renders — this is what made the carousel play/pause (and the
   old video mute) buttons show both icons side by side. */
svg[hidden] { display: none !important; }
/* Anti-flash for non-Chinese visitors: every page's HTML ships in Chinese and
   js/i18n.js swaps the text in afterwards, so a visitor who picked EN/JA saw
   a split-second of Chinese on every navigation. An inline <head> script sets
   html[data-i18n-pending] when the stored language isn't zh; applyI18n()
   removes it once the page is translated (with a 1.5s safety timeout). */
html[data-i18n-pending] body { visibility: hidden; }

body {
  margin: 0;
  background: radial-gradient(120% 120% at 50% -10%, #2a1218 0%, var(--color-bg) 55%) fixed;
  color: var(--color-text);
  font-family: var(--font-body);
  line-height: 1.65;
  -webkit-font-smoothing: antialiased;
}

img { max-width: 100%; display: block; }
a { color: inherit; text-decoration: none; }

.container {
  max-width: var(--max-width);
  margin: 0 auto;
  padding: 0 24px;
}

/* Wider variant for the OFC core-rules "feature" band (v9) — the standard
   .container (1160px) reads as narrow once this block gets a full-bleed
   background of its own, so it gets more breathing room instead. Capped
   well short of edge-to-edge so the text column and the demo widget don't
   stretch into unreadably long lines / awkward whitespace on ultra-wide
   screens. */
.container-wide {
  max-width: 1520px;
  margin: 0 auto;
  padding: 0 24px;
}

h1, h2, h3, h4 {
  font-family: var(--font-heading);
  font-weight: 700;
  line-height: 1.25;
  margin: 0 0 16px;
}

h1 { font-size: clamp(2.1rem, 4vw, 3.4rem); }
h2 { font-size: clamp(1.6rem, 3vw, 2.3rem); }
h3 { font-size: 1.25rem; }

p { margin: 0 0 16px; color: var(--color-text-muted); }

.eyebrow {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  font-size: var(--fs-xs);
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--color-gold-light);
  font-weight: 600;
  margin-bottom: 14px;
}
.eyebrow::before {
  content: "";
  width: 18px;
  height: 2px;
  background: var(--color-gold);
  display: inline-block;
}

/* --- Buttons --- */
.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 10px;
  padding: 14px 28px;
  border-radius: 999px;
  font-weight: 600;
  font-size: 0.98rem;
  cursor: pointer;
  border: 1px solid transparent;
  transition: transform 0.15s ease, box-shadow 0.15s ease, background 0.15s ease;
  white-space: nowrap;
}
.btn:hover { transform: translateY(-2px); }

.btn-primary {
  background: linear-gradient(135deg, var(--color-gold-light), var(--color-gold));
  color: #16241c;
  box-shadow: 0 10px 24px rgba(212, 175, 55, 0.28);
}
.btn-primary:hover { box-shadow: 0 14px 30px rgba(212, 175, 55, 0.4); }

/* v29: give [disabled] buttons a clearly different look — previously a
   disabled .btn-primary (e.g. "確認排列" before the row is full) kept its
   full gold gradient and just silently stopped responding to clicks,
   which read as broken rather than "not ready yet". Flattening the color
   and dimming it makes disabled vs. active obvious at a glance. */
.btn[disabled] {
  cursor: not-allowed;
  filter: grayscale(0.5);
  opacity: 0.55;
}
.btn-primary[disabled] {
  background: rgba(245, 241, 230, 0.14);
  color: rgba(245, 241, 230, 0.55);
  box-shadow: none;
}
.btn-primary[disabled]:hover { box-shadow: none; transform: none; }

.btn-outline {
  background: transparent;
  border-color: rgba(245, 241, 230, 0.35);
  color: var(--color-text);
}
.btn-outline:hover { border-color: var(--color-gold); color: var(--color-gold-light); }

.btn-block { width: 100%; }
.btn-sm { padding: 10px 18px; font-size: 0.88rem; }

/* --- Store badges --- */
.store-badges {
  display: flex;
  gap: 14px;
  flex-wrap: wrap;
}
.store-badge {
  display: inline-flex;
  align-items: center;
  gap: 10px;
  padding: 11px 20px;
  border-radius: var(--radius-sm);
  background: #0d1f18;
  border: 1px solid rgba(245, 241, 230, 0.18);
  color: var(--color-text);
  font-weight: 600;
  transition: border-color 0.15s ease, transform 0.15s ease;
}
.store-badge:hover { border-color: var(--color-gold); transform: translateY(-2px); }
.store-badge svg { width: 22px; height: 22px; flex-shrink: 0; }
.store-badge .badge-text { display: flex; flex-direction: column; line-height: 1.1; text-align: left; }
.store-badge .badge-text small { font-size: 0.68rem; color: var(--color-text-faint); font-weight: 500; }
.store-badge .badge-text span { font-size: 0.98rem; }

/* --- Navigation --- */
.site-header {
  position: sticky;
  top: 0;
  z-index: 100;
  backdrop-filter: blur(10px);
  background: rgba(10, 10, 13, 0.82);
  border-bottom: 1px solid var(--color-border);
}
.nav {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 16px 24px;
}
.brand {
  display: flex;
  align-items: center;
  gap: 10px;
  font-family: var(--font-heading);
  font-weight: 700;
  font-size: 1.15rem;
  color: var(--color-text);
}
.brand-mark {
  width: 38px;
  height: 38px;
  border-radius: 10px;
  background: linear-gradient(145deg, var(--color-primary-light), var(--color-primary));
  display: flex;
  align-items: center;
  justify-content: center;
  box-shadow: inset 0 0 0 1px rgba(212,175,55,0.4);
  font-size: 1.1rem;
  overflow: hidden;
  flex-shrink: 0;
}
.brand-mark img { width: 100%; height: 100%; object-fit: cover; display: block; }
.nav-links {
  display: flex;
  gap: 34px;
  list-style: none;
  margin: 0;
  padding: 0;
}
.nav-links a {
  font-weight: 500;
  color: var(--color-text-muted);
  transition: color 0.15s ease;
  position: relative;
}
.nav-links a:hover, .nav-links a.active { color: var(--color-gold-light); }
.nav-cta { display: flex; align-items: center; gap: 16px; }
.nav-toggle {
  display: none;
  flex-direction: column;
  gap: 5px;
  background: none;
  border: none;
  cursor: pointer;
  padding: 6px;
}
.nav-toggle span { width: 24px; height: 2px; background: var(--color-text); display: block; }

/* --- Language switch: dropdown menu (v13) ---
   Was a 3-way sliding toggle; now a single trigger button showing the
   current language, which opens a downward dropdown listing all three
   in a fixed order (EN / 中 / 日 top-to-bottom, per spec). Open/close is
   handled in js/i18n.js (initLangToggle); selecting an item still calls
   the same ofcSetLang()/applyI18n() as before, so localStorage
   persistence and the I18N dictionary itself are unchanged. */
.lang-switch { position: relative; }
.lang-switch-toggle {
  display: inline-flex;
  align-items: center;
  gap: 7px;
  appearance: none;
  border: 1px solid rgba(245, 241, 230, 0.28);
  background: rgba(0, 0, 0, 0.2);
  color: var(--color-text);
  font-family: var(--font-body);
  font-size: 0.8rem;
  font-weight: 700;
  padding: 8px 14px;
  border-radius: 999px;
  cursor: pointer;
  transition: border-color 0.15s ease, color 0.15s ease;
}
.lang-switch-toggle:hover { border-color: var(--color-gold); color: var(--color-gold-light); }
.lang-switch-caret {
  width: 10px;
  height: 10px;
  flex-shrink: 0;
  transition: transform 0.2s ease;
}
.lang-switch.open .lang-switch-caret { transform: rotate(180deg); }
.lang-switch-menu {
  position: absolute;
  top: calc(100% + 8px);
  left: 50%;
  min-width: 92px;
  display: flex;
  flex-direction: column;
  background: var(--color-bg-alt);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-sm);
  box-shadow: var(--shadow-card);
  overflow: hidden;
  z-index: 200;
  opacity: 0;
  transform: translateX(-50%) translateY(-6px) scaleY(0.94);
  transform-origin: top;
  pointer-events: none;
  transition: opacity 0.15s ease, transform 0.15s ease;
}
.lang-switch.open .lang-switch-menu {
  opacity: 1;
  transform: translateX(-50%) translateY(0) scaleY(1);
  pointer-events: auto;
}
.lang-switch-menu button,
.lang-switch-menu a {
  display: block;
  text-decoration: none;
  appearance: none;
  border: none;
  background: transparent;
  color: var(--color-text-muted);
  font-family: var(--font-body);
  font-size: 0.82rem;
  font-weight: 700;
  padding: 10px 16px;
  text-align: center;
  cursor: pointer;
  transition: background 0.15s ease, color 0.15s ease;
}
.lang-switch-menu > * + * { border-top: 1px solid var(--color-border); }
.lang-switch-menu button:hover,
.lang-switch-menu a:hover { background: rgba(212, 175, 55, 0.12); color: var(--color-gold-light); }
.lang-switch-menu button.active,
.lang-switch-menu a.active {
  background: linear-gradient(135deg, var(--color-gold-light), var(--color-gold));
  color: #16241c;
}

/* --- Header social icons: compact version of the footer's social-row --- */
.social-row-sm { display: flex; gap: 8px; }
.social-row-sm a { width: 32px; height: 32px; }

/* --- Top video banner: Steam-style full-bleed hero video/key-art strip ---
   Steam's storefront front page opens with an edge-to-edge banner (image or
   looping video) that spans the full browser width, not the site's usual
   1160px container. We replicate that here: full-bleed <video>, autoplay
   muted loop, dark gradient at the bottom for legibility, small tag chip +
   mute toggle bottom-left/bottom-right like Steam's own trailer controls. */
.video-hero {
  position: relative;
  width: 100%;
  aspect-ratio: 16 / 9;
  max-height: 78vh;
  overflow: hidden;
  background: #000;
  isolation: isolate; /* keeps the blurred ::before backdrop inside this box */
}
/* Never crop the trailer: event videos carry title text at the very top and
   key art at the bottom, so `cover` (which trimmed top/bottom once the 78vh
   cap kicked in on wide screens) cut the words off. The video is now
   `contain`ed, and any leftover side space is filled with a blurred, dimmed
   copy of the poster frame so the banner still reads as full-bleed.
   When swapping the monthly video, refresh hero-video-poster.jpg too —
   this backdrop uses it. */
.video-hero::before {
  content: "";
  position: absolute;
  inset: -60px;
  background: url("../assets/images/hero-video-poster.jpg") center / cover no-repeat;
  filter: blur(24px) brightness(0.62) saturate(1.15);
  z-index: -1;
}
.video-hero-frame {
  position: relative;
  height: 100%;
  aspect-ratio: 16 / 9;
  max-width: 100%;
  margin: 0 auto;
  /* lets the CTA size itself in cqw = % of the visible video width */
  container-type: inline-size;
}
.video-hero-media {
  width: 100%;
  height: 100%;
  object-fit: contain;
  display: block;
}
.video-hero-overlay {
  position: absolute;
  inset: 0;
  background: linear-gradient(0deg, rgba(5, 5, 6, 0.88) 0%, rgba(5, 5, 6, 0.25) 22%, transparent 45%);
  pointer-events: none;
}
.video-hero-bar {
  position: absolute;
  left: 0; right: 0; bottom: 0;
  display: flex;
  align-items: center;
  justify-content: flex-start;
  padding: 0 3cqw 2.6cqw;
  pointer-events: none; /* only the CTA itself should catch clicks */
}
/* Desktop/tablet "Play Now" CTA on top of the trailer (inside
   .video-hero-frame, so it always sits on the footage). Styled as an
   app-store-like "dossier tab": dark glass + gold hairline, app icon,
   two-line label and a gold arrow disc — reads as a game download action
   rather than a generic pill. Hidden on phones, where the video is short
   and the A section's store badges are right below it. */
/* Everything below is in em off a font-size tied to the video width
   (cqw), so the button keeps the same footprint relative to the footage —
   about 22% of the video width — and never collides with the event art
   (the Ghost Heist medal starts at ~27% from the left). */
.video-hero-cta {
  pointer-events: auto;
  font-size: clamp(11px, 1.15cqw, 17px);
  display: inline-flex;
  align-items: center;
  gap: 0.85em;
  padding: 0.6em 0.7em 0.6em 0.6em;
  border-radius: 1.15em;
  background: linear-gradient(135deg, rgba(25, 22, 28, 0.82), rgba(10, 10, 13, 0.72));
  -webkit-backdrop-filter: blur(10px);
  backdrop-filter: blur(10px);
  border: 1px solid rgba(212, 175, 55, 0.55);
  box-shadow: inset 0 1px 0 rgba(242, 216, 121, 0.18), 0 12px 32px rgba(0, 0, 0, 0.55);
  color: var(--color-text);
  position: relative;
  overflow: hidden;
  transition: transform 0.2s ease, border-color 0.2s ease, box-shadow 0.2s ease;
}
/* light sweep across the button on hover */
.video-hero-cta::after {
  content: "";
  position: absolute;
  top: 0; bottom: 0; left: -60%;
  width: 40%;
  background: linear-gradient(100deg, transparent, rgba(242, 216, 121, 0.18), transparent);
  transform: skewX(-18deg);
  transition: left 0.6s ease;
  pointer-events: none;
}
.video-hero-cta:hover,
.video-hero-cta:focus-visible {
  transform: translateY(-2px);
  border-color: var(--color-gold-light);
  box-shadow: inset 0 1px 0 rgba(242, 216, 121, 0.25), 0 16px 36px rgba(0, 0, 0, 0.6), 0 0 28px rgba(212, 175, 55, 0.35);
  outline: none;
}
.video-hero-cta:hover::after { left: 120%; }
.video-hero-cta-icon {
  width: 3.3em;
  height: 3.3em;
  border-radius: 0.75em;
  display: block;
  flex: 0 0 auto;
  box-shadow: 0 0 0 1px rgba(242, 216, 121, 0.35), 0 4px 10px rgba(0, 0, 0, 0.45);
}
.video-hero-cta-text {
  display: flex;
  flex-direction: column;
  gap: 0.2em;
  line-height: 1.15;
  white-space: nowrap;
}
.video-hero-cta-text strong {
  font-family: var(--font-heading);
  font-size: 1.32em;
  font-weight: 800;
  letter-spacing: 0.03em;
  color: var(--color-gold-light);
}
/* Japanese "今すぐプレイ" is wider than 立即遊玩 / Play Now */
:lang(ja) .video-hero-cta-text strong { font-size: 1.16em; letter-spacing: 0.01em; }
.video-hero-cta-text small {
  font-size: 0.8em;
  color: var(--color-text-muted);
  letter-spacing: 0.02em;
}
.video-hero-cta-arrow {
  width: 2.7em;
  height: 2.7em;
  margin-left: 0.3em;
  border-radius: 50%;
  flex: 0 0 auto;
  display: flex;
  align-items: center;
  justify-content: center;
  background: linear-gradient(135deg, var(--color-gold-light), var(--color-gold));
  color: #16120a;
  box-shadow: 0 4px 12px rgba(212, 175, 55, 0.35);
  transition: transform 0.2s ease;
}
.video-hero-cta-arrow svg { width: 1.3em; height: 1.3em; }
.video-hero-cta:hover .video-hero-cta-arrow { transform: translateX(3px); }
/* Narrow videos (tablets): the 11px font floor would make the button take
   30%+ of the frame, so drop the sub-line, then the app icon. */
@container (max-width: 960px) {
  .video-hero-cta-text small { display: none; }
}
@container (max-width: 720px) {
  .video-hero-cta-icon { display: none; }
  .video-hero-cta { padding-left: 1.1em; }
}
@media (prefers-reduced-motion: reduce) {
  .video-hero-cta, .video-hero-cta::after, .video-hero-cta-arrow { transition: none; }
}
@media (max-width: 700px) {
  .video-hero { max-height: none; } /* keep 16:9 so nothing is cropped */
  .video-hero-bar { display: none; }
}

@media (max-width: 860px) {
  .nav-links {
    position: absolute;
    top: 100%; left: 0; right: 0;
    background: var(--color-bg-alt);
    flex-direction: column;
    gap: 0;
    border-bottom: 1px solid var(--color-border);
    max-height: 0;
    overflow: hidden;
    transition: max-height 0.25s ease;
  }
  .nav-links.open { max-height: 400px; }
  .nav-links li { border-top: 1px solid var(--color-border); }
  .nav-links a { display: block; padding: 16px 24px; }
  .nav-cta .btn-primary { display: none; }
  .nav-cta .social-row-sm { display: none; }
  .nav-toggle { display: flex; }
  .brand { font-size: 0.95rem; }
  .lang-switch-toggle { padding: 6px 10px; font-size: 0.72rem; }
}

/* --- Hero --- */
.hero {
  padding: 96px 0 72px;
  position: relative;
  overflow: hidden;
}
.hero::after {
  content: "";
  position: absolute;
  inset: -40% -10% auto -10%;
  height: 480px;
  background: radial-gradient(closest-side, rgba(122, 22, 38, 0.4), transparent 70%);
  z-index: -1;
}
.hero-grid {
  display: grid;
  grid-template-columns: 1.05fr 0.95fr;
  gap: 56px;
  align-items: center;
}
@media (max-width: 900px) {
  .hero-grid { grid-template-columns: 1fr; }
}
.hero-art {
  position: relative;
  display: flex;
  justify-content: center;
}
.phone-frame {
  width: min(300px, 78vw);
  aspect-ratio: 9 / 19.5;
  border-radius: 42px;
  background: linear-gradient(160deg, var(--color-surface-2), var(--color-bg-alt));
  border: 6px solid #18131a;
  box-shadow: var(--shadow-glow);
  position: relative;
  overflow: hidden;
}
.phone-frame img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}
.phone-frame .screen-content {
  position: absolute;
  inset: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 14px;
  padding: 24px;
  text-align: center;
}
.card-suit-row { display: flex; gap: 10px; font-size: 1.6rem; }
.hero-stats {
  display: flex;
  gap: 32px;
  margin-top: 36px;
  flex-wrap: wrap;
}
.hero-stat strong {
  display: block;
  font-family: var(--font-heading);
  font-size: 1.6rem;
  color: var(--color-gold-light);
}
.hero-stat span { font-size: var(--fs-sm); color: var(--color-text-faint); }

/* --- how-to-play.html hero portrait (v12) ---
   Scoped under its own htp- prefix rather than reusing the homepage's
   .hero-grid/.hero-art (different column ratio and vertical alignment —
   this one bottom-anchors so the figure's feet sit level with the last
   line of body copy, per the "plain, image bleeds off the right edge"
   option the user picked over a CTA-banner-style bordered card). See
   how-to-play-page-task-context.md "v12". */
.htp-hero-grid {
  display: grid;
  grid-template-columns: 1.15fr 0.85fr;
  gap: 24px;
  /* v18 draft: was flex-end (bottom-anchored to the portrait's feet), which
     left a large empty gap above the shorter text column whenever the
     portrait ran taller than the copy block — read as "hero feels empty".
     Center-aligning splits that leftover space evenly instead. */
  align-items: center;
}
/* v20: enlarged variant — was going to be truly edge-to-edge like
   .video-hero, but on a wide monitor that put SO much raw width between
   the (now short, max-width-640px) text block and the portrait that it
   just moved the "middle feels empty" problem instead of fixing it —
   confirmed after two rounds of user feedback (v20-fix's flex-end made it
   worse, not better). Settled on .container-wide's own 1520px cap,
   centered, same as the OFC-rules feature band elsewhere on this page —
   wider/bigger than the original 1160px hero container, but not
   literally full-viewport, so the gap between text and portrait stays
   bounded no matter how wide the browser window is. */
.htp-hero-grid-full {
  width: 100%;
  max-width: 1520px;
  margin: 0 auto;
  box-sizing: border-box;
  grid-template-columns: minmax(0, 640px) 1fr;
  padding: 0 clamp(24px, 6vw, 72px);
}
.htp-hero-portrait { display: flex; justify-content: center; }
.htp-hero-portrait img {
  /* v20: bumped up now that the grid has the extra width to use. */
  height: clamp(380px, 42vw, 640px);
  width: auto;
  display: block;
  filter: drop-shadow(0 24px 40px rgba(0, 0, 0, 0.55));
}
@media (max-width: 900px) {
  .htp-hero-grid-full { grid-template-columns: 1fr; padding: 0 clamp(24px, 6vw, 72px); }
  .htp-hero-grid-full .htp-hero-portrait { justify-content: center; }
}
@media (max-width: 760px) {
  .htp-hero-grid { grid-template-columns: 1fr; text-align: center; justify-items: center; }
  .htp-hero-portrait img { height: clamp(260px, 60vw, 380px); margin-top: 8px; }
}

/* --- how-to-play.html hero copy (v13, stats row removed in v18) ---
   The heading and paragraph get a scoped size bump beyond the page-wide
   .page-how-to-play type scale. v13's hero-stats row (three quick facts)
   was tried to fill perceived empty space next to the portrait but felt
   like forced filler, so v18 removed it and fixed the real cause instead
   (see .htp-hero-grid's align-items change above). See
   how-to-play-page-task-context.md "v13"/"v18". */
/* v20: another size bump on top of v13's — larger ceiling on the h1, and a
   scoped-up eyebrow tag to match (this page's eyebrow already has its own
   v16 letter-spacing/size tweak under .page-how-to-play .eyebrow; this
   adds a further bump specific to the hero tag only). Copy is shorter
   now (see how-to-play.html), so the paragraph's max-width can grow
   without the block reading as a dense wall of text. */
.htp-hero-text .eyebrow { font-size: calc(var(--fs-xs) * 1.35); }
.htp-hero-text h1 { font-size: clamp(2.6rem, 5.6vw, 4.8rem); }
.htp-hero-text p {
  max-width: 640px;
  font-size: 1.4rem;
  line-height: 1.65;
  color: var(--color-text);
}

/* --- Brand trust section: "HQ transmission" dossier styling ---
   A coded-message treatment for the intro copy (monospace bracket tag,
   thin gold rule) paired with the character portrait, floating on a
   soft radial glow so a transparent-background PNG reads naturally
   against the dark page. */
.trust {
  position: relative;
  overflow: hidden;
}
.trust-grid {
  display: grid;
  grid-template-columns: 1.1fr 0.9fr;
  gap: 56px;
  align-items: center;
}
@media (max-width: 900px) {
  .trust-grid { grid-template-columns: 1fr; }
}
.transmission-tag {
  display: inline-block;
  font-family: ui-monospace, 'SFMono-Regular', 'Courier New', monospace;
  font-size: var(--fs-xs);
  font-weight: 700;
  letter-spacing: 0.04em;
  color: var(--color-gold-light);
  background: rgba(212, 175, 55, 0.1);
  border: 1px solid var(--color-border);
  border-left: 3px solid var(--color-gold);
  padding: 8px 16px;
  border-radius: 4px;
  margin-bottom: 22px;
}
/* Copy hierarchy inside the dossier block: an opening "briefing" line reads
   slightly larger and brighter than the supporting detail below it, so the
   four paragraphs don't all compete at the same weight. */
.transmission p {
  font-size: var(--fs-base);
  max-width: 520px;
}
.transmission-lead {
  font-size: var(--fs-md);
  color: var(--color-text);
  font-weight: 500;
}
.transmission-closing {
  font-size: var(--fs-lg) !important;
  line-height: 1.55;
  color: var(--color-gold-light) !important;
  font-weight: 800;
  letter-spacing: 0.01em;
  border-top: 1px dashed var(--color-border);
  padding-top: 18px;
  margin-top: 10px !important;
}
.trust-portrait {
  position: relative;
  display: flex;
  justify-content: center;
}
.trust-portrait::before {
  content: "";
  position: absolute;
  inset: 10% -10% auto -10%;
  height: 70%;
  background: radial-gradient(closest-side, rgba(212, 175, 55, 0.22), transparent 70%);
  z-index: -1;
}
.trust-portrait img {
  width: min(320px, 80vw);
  height: auto;
  filter: drop-shadow(0 24px 40px rgba(0, 0, 0, 0.55));
}

/* About us hero uses the full crew-poster art (landing-hero-crew.png),
   which reads better bigger than the single-portrait default above —
   scoped to .page-about only so index.html's boss.png hero is untouched. */
.page-about .trust-portrait img {
  width: min(560px, 94vw);
}

/* --- Carousel engine (shared: B feature cards + C collectibles, v26) ---
   Both sections used to be different things — B was a static 4x2 CSS
   grid with no interaction at all, C was a native overflow-x/scroll-snap
   scroller with manual drag/arrows only. v26 upgrades both to the same
   JS-driven sliding carousel (see initCarousel() in js/main.js): a
   full-bleed viewport that breaks out of .container so more cards fit
   edge-to-edge on wide screens, click-a-dot-to-jump OR wait for
   autoplay to auto-advance, a play/pause toggle, and a true seamless
   infinite loop (cloned lead/trail slides + a silent no-transition jump
   at the seam once the visitor scrolls past the real first/last card).
   Each section only tunes --slides-visible per breakpoint; the slide-
   width formula below reads it automatically, so B and C can each show
   a different number of cards at once without duplicating any JS. */
.carousel-wrap { position: relative; width: 100%; }
.carousel-viewport {
  overflow: hidden;
  /* Full-bleed: unlike the rest of the page this wrap is NOT inside
     .container (see index.html), so it runs edge-to-edge. Side padding
     here — instead of on a container — is what keeps the first/last
     card off the very edge of the browser while everything in between
     still spans the full window width. */
  padding: 8px clamp(20px, 4vw, 64px) 8px;
}
.carousel-track {
  display: flex;
  gap: var(--carousel-gap, 24px);
  transition: transform 0.55s cubic-bezier(0.4, 0, 0.2, 1);
  cursor: grab;
  /* Lets the browser still handle vertical page scroll on touch while
     main.js's pointer handlers take over horizontal dragging. */
  touch-action: pan-y;
}
/* Applied for exactly one frame during the seamless-loop jump so the
   track snaps to the mirrored real slide with no visible animation. */
.carousel-track.no-transition { transition: none !important; }
.carousel-track.is-dragging { transition: none !important; cursor: grabbing; }

.carousel-controls {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 18px;
  margin-top: 28px;
  flex-wrap: wrap;
}
.carousel-dots { display: flex; gap: 8px; flex-wrap: wrap; justify-content: center; }
@media (max-width: 620px) {
  /* 8 dots x 34px + the play/pause button overflowed a ~390px phone and
     wrapped the dots onto a second line; narrower dots keep one row. */
  .carousel-controls { gap: 14px; }
  .carousel-dots { gap: 6px; }
  /* .carousel-dots prefix: this block sits above the base .carousel-dot
     rule, so it needs the extra specificity to win (it silently lost before). */
  .carousel-dots .carousel-dot { width: 24px; }
}
.carousel-dot {
  appearance: none;
  border: none;
  padding: 0;
  width: 34px;
  height: 5px;
  border-radius: 3px;
  background: rgba(245, 241, 230, 0.18);
  position: relative;
  overflow: hidden;
  cursor: pointer;
}
.carousel-dot-fill {
  position: absolute;
  inset: 0;
  width: 0%;
  border-radius: 3px;
  background: linear-gradient(90deg, var(--color-gold), var(--color-gold-light));
}
.carousel-dot.is-active .carousel-dot-fill { width: 100%; }
/* .is-animating is only added once autoplay is actually running, so a
   manual dot-click / arrow-click jump shows a solid filled dot (no
   progress sweep) until the next autoplay cycle picks back up. */
.carousel-dot.is-active.is-animating .carousel-dot-fill {
  width: 0%;
  animation: carousel-dot-fill var(--autoplay-ms, 5000ms) linear forwards;
}
.carousel-wrap.is-paused .carousel-dot.is-active.is-animating .carousel-dot-fill {
  animation-play-state: paused;
}
@keyframes carousel-dot-fill {
  from { width: 0%; }
  to { width: 100%; }
}
.carousel-playpause {
  flex: 0 0 auto;
  width: 34px;
  height: 34px;
  border-radius: 50%;
  border: 1px solid var(--color-border);
  background: rgba(10, 10, 13, 0.55);
  color: var(--color-text);
  display: flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
  transition: border-color 0.15s ease, color 0.15s ease;
}
.carousel-playpause:hover { border-color: var(--color-gold); color: var(--color-gold-light); }
.carousel-playpause svg { width: 15px; height: 15px; }

/* --- Feature carousel (B section, v26 redesign) ---
   Was a static 4-column x 2-row CSS grid: 8 cards, all visible at once,
   no interaction, capped at the site's 1160px container width. Now a
   full-bleed sliding carousel showing --slides-visible cards at a time;
   .feature-card keeps its v22 visual styling (image + title + desc) and
   becomes the carousel's slide element directly — no extra wrapper div
   needed. Cards are noticeably bigger than the old grid simply because
   the viewport is wider and each one gets more of it; the source images
   themselves are unchanged for now (still 900x675 4:3 jpgs) — a later
   pass can re-crop/re-export them at higher resolution once the visible
   card count here is finalized. */
.carousel-wrap--features { --carousel-gap: 24px; --slides-visible: 5; }
@media (max-width: 1100px) { .carousel-wrap--features { --slides-visible: 4; } }
@media (max-width: 860px)  { .carousel-wrap--features { --slides-visible: 3; } }
@media (max-width: 620px)  { .carousel-wrap--features { --slides-visible: 2; } }
@media (max-width: 440px)  { .carousel-wrap--features { --slides-visible: 1; } }
.carousel-wrap--features .feature-card {
  flex: 0 0 calc((100% - (var(--slides-visible) - 1) * var(--carousel-gap)) / var(--slides-visible));
}
/* Enlarged from the old grid's compact card copy now that each card
   gets meaningfully more width on a full-bleed carousel. */
.carousel-wrap--features .feature-card-body { padding: 24px 26px 30px; }
.carousel-wrap--features .feature-card-body h3 { font-size: 1.2rem; }
.carousel-wrap--features .feature-card-body p { font-size: var(--fs-base); }

/* --- Collectibles carousel (C section, v26 upgrade) ---
   v13-v25: native overflow-x + scroll-snap track, manual drag/arrows
   only, hard-stopped at both ends (no loop), no autoplay. v26 switches
   this to the same shared carousel engine as the B section above:
   full-bleed, more cards visible at once, autoplay + click-a-dot, and a
   true infinite loop instead of stopping dead at either end. Card
   visuals themselves (.collectible-slide/-thumb/-name) are unchanged. */
.carousel-wrap--collectibles { --carousel-gap: 22px; --slides-visible: 7; }
@media (max-width: 1300px) { .carousel-wrap--collectibles { --slides-visible: 6; } }
@media (max-width: 1100px) { .carousel-wrap--collectibles { --slides-visible: 5; } }
@media (max-width: 860px)  { .carousel-wrap--collectibles { --slides-visible: 4; } }
@media (max-width: 620px)  { .carousel-wrap--collectibles { --slides-visible: 3; } }
@media (max-width: 440px)  { .carousel-wrap--collectibles { --slides-visible: 2; } }
.carousel-wrap--collectibles .carousel-viewport {
  /* Kept from v13: extra top padding so the Nameplate 063 reveal
     animation can still poke out above its own frame with no clipping. */
  padding-top: 34px;
}
.collectible-slide {
  flex: 0 0 calc((100% - (var(--slides-visible) - 1) * var(--carousel-gap)) / var(--slides-visible));
  background: rgba(245, 241, 230, 0.05);
  backdrop-filter: blur(14px);
  -webkit-backdrop-filter: blur(14px);
  border: 1px solid rgba(212, 175, 55, 0.22);
  border-radius: var(--radius);
  padding: 22px 20px 20px;
  text-align: center;
  box-shadow: 0 18px 40px rgba(0, 0, 0, 0.3);
  transition: transform 0.2s ease, border-color 0.2s ease, background 0.2s ease;
}
.collectible-slide:hover {
  transform: translateY(-4px);
  border-color: rgba(212, 175, 55, 0.5);
  background: rgba(245, 241, 230, 0.08);
}
.collectible-slide-thumb {
  aspect-ratio: 1 / 1;
  display: flex;
  align-items: center;
  justify-content: center;
  margin-bottom: 16px;
}
.collectible-slide-thumb img {
  max-width: 100%;
  max-height: 100%;
  object-fit: contain;
  filter: drop-shadow(0 12px 22px rgba(0, 0, 0, 0.45));
}
/* Prototype: lets the "Agent Crate" reveal animation break out of its own
   frame — the helicopter pokes above the square thumb box, echoing the
   hair-bleed treatment on the D-section agent portrait
   (.cta-agent-portrait img). align-items: flex-end anchors the crate near
   the box's bottom edge instead of the default vertical centering, then
   scale(1.45) grows the image upward from that same bottom edge
   (transform-origin: 50% 100%) so only the top — where the helicopter is —
   extends past the frame. See .carousel-wrap--collectibles .carousel-viewport's
   padding-top for the extra room this needs.
   v21: the source crop was recentered — the very first v20 crop was
   centered on the union of the helicopter's full left/right swing, but the
   resting/closed-crate pose (shown most of the time) actually sits ~24px
   left of that swing's midpoint, so the crate visibly hung left of center.
   Recropped around the resting pose's own content center instead (still
   wide enough to keep every frame's swing extremes uncropped), and backed
   the scale down slightly from 1.55 to 1.45 per feedback that the previous
   zoom felt a bit too tight. */
.collectible-slide-thumb--reveal {
  align-items: flex-end;
  position: relative;
  /* Higher than .carousel-arrow's z-index: 5 — during some frames the
     helicopter swings far enough left/right that it would otherwise
     render underneath the prev/next arrow button instead of over it. */
  z-index: 6;
}
.collectible-slide-thumb--reveal img {
  /* translateY lifts the crate so its base sits on the same line as the
     other nameplates (bottom ~77% of the square thumb, like
     nameplate-863.png). Without it the bottom-anchored 1.45x scale put the
     crate ~17% of the thumb lower than its neighbours. The % is of the
     img's own height (480x334 -> ~70% of the thumb), applied before scale. */
  transform: translateY(-24.7%) scale(1.45);
  transform-origin: 50% 100%;
}
.collectible-slide-name {
  display: block;
  font-family: var(--font-heading);
  font-size: var(--fs-sm);
  font-weight: 700;
  letter-spacing: 0.02em;
  color: var(--color-gold-light);
}
.carousel-arrow {
  position: absolute;
  top: 42%;
  transform: translateY(-50%);
  width: 56px;
  height: 56px;
  border-radius: 50%;
  background: rgba(10, 10, 13, 0.72);
  border: 1px solid var(--color-border);
  color: var(--color-text);
  display: flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
  z-index: 5;
  transition: border-color 0.15s ease, color 0.15s ease;
}
.carousel-arrow svg { width: 26px; height: 26px; }
.carousel-arrow:hover { border-color: var(--color-gold); color: var(--color-gold-light); }
/* v26: the carousel is now full-bleed (its viewport is no longer capped
   at the 1160px .container), so the arrows sit near the padded edge of
   the viewport rather than just outside a narrow card row. They also
   stay visible at every breakpoint now — the old overflow-x scroller's
   native touch-swipe is gone now that the track is JS-transformed, so
   the arrows (plus the JS pointer-drag swipe in main.js) are the only
   way to navigate by touch. */
.carousel-arrow-prev { left: clamp(4px, 1.5vw, 20px); }
.carousel-arrow-next { right: clamp(4px, 1.5vw, 20px); }
@media (max-width: 620px) {
  .carousel-arrow { width: 44px; height: 44px; }
  .carousel-arrow svg { width: 20px; height: 20px; }
}

/* --- Sections --- */
.section { padding: 80px 0; }
.section-alt { background: linear-gradient(180deg, transparent, rgba(18, 51, 38, 0.55) 12%, rgba(18, 51, 38, 0.55) 88%, transparent); }
.section-head { max-width: 640px; margin: 0 auto 48px; text-align: center; }
.section-head p { margin: 0; font-size: var(--fs-base); }

/* --- Feature grid --- */
.grid {
  display: grid;
  gap: 24px;
}
.grid-3 { grid-template-columns: repeat(3, 1fr); }
.grid-2 { grid-template-columns: repeat(2, 1fr); }
.grid-4 { grid-template-columns: repeat(4, 1fr); }
@media (max-width: 900px) {
  .grid-3, .grid-4 { grid-template-columns: repeat(2, 1fr); }
}
@media (max-width: 620px) {
  .grid-2, .grid-3, .grid-4 { grid-template-columns: 1fr; }
}

/* --- Scoring / royalty table (how-to-play.html) --- */
.royalty-table {
  width: 100%;
  max-width: 560px;
  border-collapse: collapse;
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-sm);
  overflow: hidden;
  font-variant-numeric: tabular-nums;
}
.royalty-table caption {
  caption-side: top;
  text-align: left;
  font-family: ui-monospace, 'SFMono-Regular', 'Courier New', monospace;
  font-size: var(--fs-xs);
  font-weight: 700;
  letter-spacing: 0.04em;
  color: var(--color-gold-light);
  padding: 4px 0 12px;
}
.royalty-table th, .royalty-table td {
  text-align: left;
  padding: 10px 16px;
  border-bottom: 1px solid var(--color-border);
}
.royalty-table thead th {
  font-size: var(--fs-xs);
  font-weight: 700;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  /* v29d: was --color-text-faint (gray) -- looked inconsistent since the
     "後墩"/"中墩" headers next to it are <th class="val"> and already
     override to gold via .royalty-table .val below. Matched "牌型" to
     the same gold so the whole header row reads as one row again. */
  color: var(--color-gold-light);
  border-bottom: 1px solid var(--color-gold);
}
.royalty-table tbody th {
  font-weight: 500;
  color: var(--color-text);
}
.royalty-table tbody tr:last-child th,
.royalty-table tbody tr:last-child td { border-bottom: none; }
.royalty-table .val {
  font-family: ui-monospace, 'SFMono-Regular', 'Courier New', monospace;
  text-align: right;
  color: var(--color-gold-light);
  font-weight: 600;
}

.card {
  background: linear-gradient(165deg, var(--color-surface), var(--color-surface-2));
  border: 1px solid var(--color-border);
  border-radius: var(--radius);
  padding: 28px;
  box-shadow: var(--shadow-card);
}
.card .icon {
  width: 46px;
  height: 46px;
  border-radius: 12px;
  background: rgba(212, 175, 55, 0.14);
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 1.4rem;
  margin-bottom: 18px;
}
.card h3 { margin-bottom: 8px; }
.card p { margin-bottom: 0; font-size: var(--fs-sm); }

/* --- About "our principles" case-file cards (v28) ---
   User feedback: the plain emoji-in-circle mission cards read as a
   generic "why choose us" template block, out of step with the rest of
   the site's HQ/dossier visual language. .mission-card is a scoped
   modifier added ON TOP of the shared .card (about.html's three cards
   become class="card mission-card"; how-to-play.html's own plain .card
   usage elsewhere is untouched). It swaps the flat emoji-in-circle icon
   for a monospace case-file number badge with a clipped corner (like a
   stamped tab), adds a gold left rule, and a very faint diagonal hatch
   texture reminiscent of dossier paper. No new colors or fonts — reuses
   --color-gold/-gold-light and the monospace stack already established
   by .transmission-tag. */
.mission-card {
  position: relative;
  border-left: 3px solid var(--color-gold);
  background-image:
    repeating-linear-gradient(135deg, rgba(212, 175, 55, 0.035) 0px, rgba(212, 175, 55, 0.035) 1px, transparent 1px, transparent 14px),
    linear-gradient(165deg, var(--color-surface), var(--color-surface-2));
}
.mission-card .icon {
  border-radius: 4px;
  background: rgba(212, 175, 55, 0.08);
  border: 1.5px solid var(--color-gold);
  clip-path: polygon(0 0, 100% 0, 100% 100%, 12px 100%, 0 calc(100% - 12px));
  font-family: ui-monospace, 'SFMono-Regular', 'Courier New', monospace;
  font-size: 1rem;
  font-weight: 700;
  letter-spacing: 0.02em;
  color: var(--color-gold-light);
}

/* --- Feature cards (v22 "Why us" redesign) ---
   Replaces the old 6-card small-icon .grid-3/.card layout with an
   8-card, bigger-image grid referencing Pokerist's "Why Pokerist?"
   section: a real screenshot/illustration fills the top of each card,
   title + description sit below. Kept as its own .feature-card class
   (not .card) so the shared .card/.icon styles used on about.html and
   how-to-play.html stay untouched. .grid-4 (shared, generic column
   count) is reused for the 4-column desktop layout. */
.feature-card {
  background: linear-gradient(165deg, var(--color-surface), var(--color-surface-2));
  border: 1px solid var(--color-border);
  border-radius: var(--radius);
  overflow: hidden;
  box-shadow: var(--shadow-card);
  display: flex;
  flex-direction: column;
}
.feature-card-img {
  width: 100%;
  aspect-ratio: 4 / 3;
  object-fit: cover;
  display: block;
}
/* the "更多玩法" table-felt photos (game-rules.html hero banners, v27)
   are wide letterbox banners (2.3–3.9:1), not the ~4:3 screenshots the
   base .feature-card-img ratio was tuned for; a modifier keeps them from
   being cropped down to a plain swatch of felt. */
.feature-card-img.is-wide {
  aspect-ratio: 2.6 / 1;
  /* v29e: user feedback -- these table-felt photos read too dark/dim in
     the "更多行動" grid. The source photos already have a baked-in dark
     vignette, on top of which .feature-card-media::after (below) adds
     its own bottom-heavy dark gradient for the logo's legibility, so the
     combined effect was darker than intended. Brightened the photos
     themselves rather than only touching the overlay, so the felt/chips
     read clearly even where the overlay is lightest (top of the card). */
  filter: brightness(1.3) saturate(1.05);
}
.feature-card-body { padding: 20px 22px 26px; }
.feature-card-body h3 { margin-bottom: 8px; font-size: 1.05rem; }
.feature-card-body p { margin-bottom: 0; font-size: var(--fs-sm); }

/* v28 — the "更多玩法" table-photo cards: game name now reads off the
   gold theme_title wordmark burned into the photo (same art used in the
   game-rules.html hero banner) instead of a plain-text <h3> below, and
   the whole card is one giant link instead of only the small button.
   .feature-card-media wraps the photo so the wordmark + a bottom-fade
   overlay (for legibility over whatever felt color the photo is) can be
   positioned on top of it, without touching the plain .feature-card-img
   usage on index.html's carousel (that one has no media wrapper). */
.feature-card-media { position: relative; }
.feature-card-media::after {
  content: "";
  position: absolute;
  inset: 0;
  /* v29e: eased every stop down (was 0.05/0.22/0.58/0.85) to go with the
     is-wide brightness boost above -- still dark enough at 66%/100% to
     keep the gold logo wordmark and the card-body text below legible,
     just not as heavy over the felt/chips near the top. */
  background: linear-gradient(180deg, rgba(10, 8, 6, 0.0) 0%, rgba(10, 8, 6, 0.14) 45%, rgba(10, 8, 6, 0.42) 66%, rgba(10, 8, 6, 0.72) 100%);
  pointer-events: none;
}
.feature-card-logo {
  /* centered rather than corner-anchored, and sitting lower-middle in the
     photo (66% down) so it lands inside the gradient's darker half for
     legibility, while reading bigger/more poster-like than a small
     bottom-left mark. */
  position: absolute;
  left: 50%;
  top: 66%;
  transform: translate(-50%, -50%);
  height: clamp(34px, 8vw, 54px);
  max-width: 82%;
  width: auto;
  object-fit: contain;
  z-index: 1;
}
/* the "7 Card Stud" wordmark is a thinner mixed-case script, not the bold
   all-caps sans the other five titles use, so at the same box height it
   reads noticeably smaller/lighter -- bump just this one up. */
.feature-card-logo--stud {
  height: clamp(40px, 9.5vw, 64px);
}
a.feature-card {
  text-decoration: none;
  color: inherit;
  transition: transform 0.15s ease, box-shadow 0.15s ease, border-color 0.15s ease;
}
a.feature-card:hover { transform: translateY(-3px); box-shadow: 0 16px 32px rgba(0, 0, 0, 0.38); border-color: var(--color-gold); }
a.feature-card:focus-visible { outline: 2px solid var(--color-gold); outline-offset: 3px; }
a.feature-card .feature-card-body .btn { pointer-events: none; }

/* --- Screenshot strip: cinematic "film reel" treatment --- */
.filmstrip-wrap {
  position: relative;
  background: #050506;
  border-radius: var(--radius);
  padding: 14px 0;
  border: 1px solid var(--color-border);
  box-shadow: var(--shadow-card);
}
.filmstrip-wrap::before,
.filmstrip-wrap::after {
  content: "";
  position: absolute;
  left: 0; right: 0;
  height: 14px;
  background-image: radial-gradient(circle, var(--color-bg) 3.5px, transparent 3.6px);
  background-size: 26px 14px;
  background-repeat: repeat-x;
  background-position: 10px center;
  opacity: 0.85;
}
.filmstrip-wrap::before { top: 0; }
.filmstrip-wrap::after { bottom: 0; }
.screenshot-strip {
  display: flex;
  gap: 18px;
  overflow-x: auto;
  padding: 18px 20px;
  scroll-snap-type: x mandatory;
}
.screenshot-strip .shot {
  flex: 0 0 auto;
  width: 200px;
  aspect-ratio: 9/19.5;
  border-radius: 6px;
  background: linear-gradient(160deg, var(--color-surface-2), var(--color-bg-alt));
  border: 3px solid #100e10;
  outline: 1px solid rgba(212, 175, 55, 0.3);
  scroll-snap-align: start;
  overflow: hidden;
  position: relative;
  box-shadow: 0 10px 26px rgba(0, 0, 0, 0.55);
}
.screenshot-strip .shot img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
  filter: saturate(1.08) contrast(1.05);
}
/* Steam「精選與推薦」風格的小標籤：不是整條說明文字，而是角落一個短的
   遊戲模式名稱小徽章（例如 OFC / BJ），疊在截圖左上角，短小、不遮擋圖片
   本身內建的行銷文字。 */
.screenshot-strip .shot .mode-tag {
  position: absolute;
  top: 10px;
  left: 10px;
  padding: 4px 11px;
  font-size: 0.72rem;
  font-weight: 800;
  letter-spacing: 0.03em;
  color: #16241c;
  background: linear-gradient(135deg, var(--color-gold-light), var(--color-gold));
  border-radius: 6px;
  box-shadow: 0 4px 10px rgba(0, 0, 0, 0.4);
  z-index: 1;
}

/* --- CTA banner ---
   Restyled as a two-column "recruitment poster": the agent portrait
   bleeds above the banner's top edge on the left, copy + CTA sit on
   the right. overflow:visible on the banner (default) lets the image
   pop out; the section itself gets extra top padding so the bleed
   never collides with the section above. */
.cta-banner {
  border-radius: var(--radius-lg);
  background: linear-gradient(135deg, var(--color-primary), #2a1218 60%, var(--color-bg-alt));
  border: 1px solid var(--color-border);
  /* Top padding was 0: with the two grid columns bottom-aligned (see
     .cta-agent-grid align-items: flex-end) and both columns roughly the
     same height, the [transmission-tag] eyebrow ended up sitting flush
     against the card's top border with no breathing room. Adding padding
     here pushes the whole row down; the portrait's margin-top below is
     increased by the same 40px so the hair-bleed above the card stays the
     same visual amount. */
  padding: 40px 48px 48px;
  box-shadow: var(--shadow-glow);
}
.cta-banner h2 { margin-bottom: 12px; }
.cta-banner .store-badges { margin-top: 28px; }
.cta-agent-grid {
  display: grid;
  grid-template-columns: 0.8fr 1.2fr;
  gap: 32px;
  align-items: flex-end;
  text-align: left;
}
.cta-agent-portrait {
  display: flex;
  justify-content: center;
  align-self: flex-end;
}
.cta-agent-portrait img {
  width: min(230px, 60vw);
  /* Source art has no feet/shoes drawn — the legs just stop and fade to a
     low-contrast shadow near the bottom of the canvas. A hard crop there
     (previous approach) still read as the legs being cut off mid-frame.
     agent-girl-fullbody-fade.png pre-bakes an alpha gradient over the last
     stretch of the pant legs so they dissolve into the card's background
     instead of ending on a visible edge — no more "floating feet" look.
     object-position: top keeps the hair-bleed at the top intact.
     v21: canvas height bumped 1130 -> 1300 (less cropped, more of the legs
     kept before the alpha fade takes over). Because the column is
     bottom-anchored (align-self: flex-end on .cta-agent-portrait, no
     explicit height elsewhere), a taller aspect-ratio at the same width
     makes the *whole image* taller and — since the bottom edge stays where
     it is — that extra height entirely pushes the top upward. That's what
     drives more of the head above the card's top edge; margin-top didn't
     need to change.
     v22: bumped again, 1300 -> 1420 (close to the source PNG's own height
     of 1436px, so the fade window was also shifted later — 1120..1420 —
     to keep the same ~300px fade length right at the new crop's bottom).
     Same bottom-anchored mechanic as v21: more canvas height alone pushes
     the head further above the card, no margin-top change needed. */
  aspect-ratio: 555 / 1420;
  object-fit: cover;
  object-position: top;
  margin-top: -104px;
  filter: drop-shadow(0 20px 34px rgba(0, 0, 0, 0.6));
}
.cta-agent-content { padding-bottom: 8px; }
@media (max-width: 780px) {
  .cta-banner { padding: 28px 28px 40px; text-align: center; }
  .cta-agent-grid { grid-template-columns: 1fr; text-align: center; justify-items: center; }
  .cta-agent-portrait img { margin-top: -76px; }
  .cta-banner .store-badges { justify-content: center; }
}

/* --- Shop --- */
.product-card {
  background: linear-gradient(165deg, var(--color-surface), var(--color-surface-2));
  border: 1px solid var(--color-border);
  border-radius: var(--radius);
  overflow: hidden;
  display: flex;
  flex-direction: column;
  box-shadow: var(--shadow-card);
}
.product-thumb {
  aspect-ratio: 4/3;
  background: radial-gradient(circle at 30% 20%, rgba(212,175,55,0.25), transparent 60%), var(--color-bg-alt);
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 2.4rem;
}
.product-body { padding: 22px; display: flex; flex-direction: column; gap: 10px; flex: 1; }
.product-tag {
  align-self: flex-start;
  font-size: 0.72rem;
  padding: 4px 10px;
  border-radius: 999px;
  background: rgba(192, 57, 43, 0.18);
  color: var(--color-red-light);
  font-weight: 700;
  letter-spacing: 0.04em;
}
.product-price {
  margin-top: auto;
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding-top: 10px;
  border-top: 1px dashed var(--color-border);
}
.product-price strong { font-size: 1.3rem; color: var(--color-gold-light); font-family: var(--font-heading); }

.notice-box {
  background: rgba(212, 175, 55, 0.08);
  border: 1px solid var(--color-border);
  border-radius: var(--radius);
  padding: 22px 26px;
  font-size: 0.92rem;
  color: var(--color-text-muted);
}
.notice-box strong { color: var(--color-gold-light); }

/* --- Blog --- */
.post-card {
  background: linear-gradient(165deg, var(--color-surface), var(--color-surface-2));
  border: 1px solid var(--color-border);
  border-radius: var(--radius);
  overflow: hidden;
  display: flex;
  flex-direction: column;
}
.post-thumb {
  aspect-ratio: 16/9;
  background: linear-gradient(135deg, rgba(122,22,38,0.55), rgba(212,175,55,0.22));
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 2rem;
  overflow: hidden;
}
.post-thumb img { width: 100%; height: 100%; object-fit: cover; display: block; }
.post-body { padding: 22px; display: flex; flex-direction: column; gap: 8px; flex: 1; }
.post-meta { font-size: 0.78rem; color: var(--color-text-faint); text-transform: uppercase; letter-spacing: 0.06em; }
.post-body h3 { margin-bottom: 4px; }
.post-body a.read-more { color: var(--color-gold-light); font-weight: 600; font-size: 0.9rem; margin-top: auto; }

.article-hero {
  padding: 64px 0 32px;
}
.article-body {
  max-width: 760px;
  margin: 0 auto;
}
.article-body h2 { margin-top: 40px; }
.article-body ul, .article-body ol { color: var(--color-text-muted); }
.article-body blockquote {
  border-left: 3px solid var(--color-gold);
  margin: 24px 0;
  padding: 6px 22px;
  color: var(--color-gold-light);
  font-style: italic;
}

/* --- FAQ --- */
.faq-item {
  border-bottom: 1px solid var(--color-border);
  padding: 20px 0;
}
.faq-item summary {
  cursor: pointer;
  font-weight: 600;
  font-family: var(--font-heading);
  list-style: none;
  display: flex;
  justify-content: space-between;
  align-items: center;
}
.faq-item summary::-webkit-details-marker { display: none; }
.faq-item summary::after {
  content: "+";
  font-size: 1.4rem;
  color: var(--color-gold);
  transition: transform 0.2s ease;
}
.faq-item[open] summary::after { transform: rotate(45deg); }
.faq-item p { margin-top: 12px; margin-bottom: 0; }

/* --- Contact --- */
.contact-grid {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 40px;
}
@media (max-width: 780px) { .contact-grid { grid-template-columns: 1fr; } }
.contact-list { list-style: none; padding: 0; margin: 0; display: flex; flex-direction: column; gap: 18px; }
.contact-list li { display: flex; gap: 14px; align-items: flex-start; }
.contact-list .icon {
  width: 40px; height: 40px; border-radius: 10px;
  background: rgba(212,175,55,0.14);
  display: flex; align-items: center; justify-content: center;
  flex-shrink: 0;
}

/* --- Footer --- */
.site-footer {
  border-top: 1px solid var(--color-border);
  padding: 56px 0 28px;
  margin-top: 40px;
}
.footer-grid {
  display: grid;
  grid-template-columns: 1.6fr 1fr 1fr;
  gap: 32px;
  margin-bottom: 40px;
}
@media (max-width: 780px) { .footer-grid { grid-template-columns: repeat(2, 1fr); } }
.footer-contact-line {
  font-size: 0.85rem;
  color: var(--color-text-faint);
}
.footer-contact-line a { color: var(--color-text-muted); }
.footer-contact-line a:hover { color: var(--color-gold-light); }
.footer-grid h4 { color: var(--color-text); font-size: 0.95rem; margin-bottom: 16px; }
.footer-links { list-style: none; padding: 0; margin: 0; display: flex; flex-direction: column; gap: 10px; }
.footer-links a { color: var(--color-text-muted); font-size: 0.9rem; }
.footer-links a:hover { color: var(--color-gold-light); }
.footer-bottom {
  display: flex;
  justify-content: space-between;
  align-items: center;
  flex-wrap: wrap;
  gap: 12px;
  padding-top: 24px;
  border-top: 1px solid var(--color-border);
  font-size: 0.82rem;
  color: var(--color-text-faint);
}
.social-row { display: flex; gap: 12px; }
.social-row a { width: 40px; height: 40px; }

/* --- Social icon buttons (header .social-row-sm + footer .social-row) ---
   Resting state stays on-brand (gunmetal disc, gold hairline, gold glyph) so
   the icons don't fight the site's red/gold palette; each platform's own
   colours only appear on hover/focus as confirmation of where the link goes.
   IG / X links ship with the `hidden` attribute until their accounts exist;
   the explicit [hidden] rule is needed because `display:flex` below would
   otherwise override the UA's [hidden]{display:none}. */
.social-row-sm a,
.social-row a {
  border-radius: 999px;
  border: 1px solid rgba(212, 175, 55, 0.38);
  background: radial-gradient(circle at 30% 25%, #2a2530, var(--color-surface) 70%);
  color: var(--color-gold);
  display: flex;
  align-items: center;
  justify-content: center;
  overflow: hidden;
  box-shadow: inset 0 1px 0 rgba(242, 216, 121, 0.12);
  transition: background 0.18s ease, border-color 0.18s ease, color 0.18s ease,
              box-shadow 0.18s ease, transform 0.18s ease;
}
.social-row-sm a[hidden],
.social-row a[hidden] { display: none; }
.social-row-sm a svg,
.social-row a svg { display: block; }
.social-row-sm a:hover,
.social-row a:hover,
.social-row-sm a:focus-visible,
.social-row a:focus-visible {
  color: var(--color-gold-light);
  border-color: var(--color-gold);
  transform: translateY(-1px);
  outline: none;
}

/* Facebook: the official 2019+ mark is a disc with the "f" knocked out and
   its stem running off the bottom edge. The SVG path *is* that disc, so the
   button itself becomes the logo: the path is painted in the disc colour and
   the button background shows through the "f".
   Rest  = gunmetal disc, gold "f", same gold hairline as IG / X
   Hover = official look: #1877f2 disc, white "f". */
.social-row-sm a.social-fb,
.social-row a.social-fb {
  /* keep the shared 1px border so all three discs are the same size;
     clip the gold fill to the content box so it only shows through the "f" */
  background: var(--color-gold);
  background-clip: content-box;
  color: var(--color-surface);
  box-shadow: none;
}
.social-row-sm a.social-fb svg,
.social-row a.social-fb svg { width: 100%; height: 100%; }
.social-row-sm a.social-fb svg path,
.social-row a.social-fb svg path { fill: currentColor; }
.social-row-sm a.social-fb:hover,
.social-row a.social-fb:hover,
.social-row-sm a.social-fb:focus-visible,
.social-row a.social-fb:focus-visible {
  background: #fff;
  color: #1877f2;
  border-color: #1877f2;
  box-shadow: 0 0 0 3px rgba(24, 119, 242, 0.3), 0 6px 16px rgba(24, 119, 242, 0.25);
}

/* Instagram: brand icon image on hover (v31d).
   Three rounds of CSS gradients (radial, then linear, then a non-animated
   linear) each fixed one browser-specific rendering glitch but the user
   kept seeing a different one (a dark unfilled gap, then a hard seam) --
   evidently specific to their browser and not reproducible here, so
   rather than keep guessing at gradient syntax, this swaps in the user's
   own reference Instagram icon as a plain background image instead of a
   CSS gradient. A raster image has no interpolation/position math to get
   wrong, so it renders identically everywhere. The glyph is baked into
   the image, so the inline SVG is just hidden on hover (kept in the
   markup for the resting on-brand gunmetal state). Source image cropped
   tight to the circle's own edges (so background-size:cover fills the
   disc with no leftover padding) and pngquant/optipng'd down to ~8.7KB. */
.social-row-sm a.social-ig:hover,
.social-row a.social-ig:hover,
.social-row-sm a.social-ig:focus-visible,
.social-row a.social-ig:focus-visible {
  background: url('../assets/images/icon-instagram-color.png') center / cover no-repeat;
  /* v31f: background-origin defaults to padding-box, so `cover` sized the
     image to fit just inside the border instead of the full border-box --
     left a thin unpainted (dark) ring between the white border and the
     image itself. border-box here makes the image's "cover" sizing use
     the full box (border included), so it reaches the true edge. */
  background-origin: border-box;
  background-clip: border-box;
  /* v31e: was `border-color: transparent` -- against the nav's near-black
     background that left the disc with no visible edge at all (FB/X both
     keep a ring on hover: FB's brand blue, X's semi-transparent white).
     Matching X's white ring here per user feedback ("放大時白邊都要在"). */
  border-color: rgba(255, 255, 255, 0.7);
  box-shadow: 0 0 0 3px rgba(214, 36, 159, 0.28), 0 6px 16px rgba(214, 36, 159, 0.22);
}
.social-row-sm a.social-ig:hover svg,
.social-row a.social-ig:hover svg,
.social-row-sm a.social-ig:focus-visible svg,
.social-row a.social-ig:focus-visible svg {
  opacity: 0;
}
.social-row-sm a.social-ig,
.social-row a.social-ig {
  transition: border-color 0.18s ease, color 0.18s ease, box-shadow 0.18s ease, transform 0.18s ease;
}
/* X: black disc + white glyph on hover (light ring so it reads on our near-black bg) */
.social-row-sm a.social-x:hover,
.social-row a.social-x:hover,
.social-row-sm a.social-x:focus-visible,
.social-row a.social-x:focus-visible {
  background: #000;
  border-color: rgba(255, 255, 255, 0.7);
  color: #fff;
  box-shadow: 0 0 0 3px rgba(255, 255, 255, 0.14), 0 6px 16px rgba(0, 0, 0, 0.5);
}

@media (prefers-reduced-motion: reduce) {
  .social-row-sm a, .social-row a { transition: none; }
  .social-row-sm a:hover, .social-row a:hover { transform: none; }
}

.age-badge {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  font-size: var(--fs-xs);
  color: var(--color-text-faint);
  border: 1px solid var(--color-border);
  padding: 6px 12px;
  border-radius: 999px;
}

.disclaimer {
  font-size: var(--fs-xs);
  color: var(--color-text-faint);
  max-width: 760px;
}

/* =========================================================
   How-to-Play interactive walkthrough (js/how-to-play-demo.js, v4)
   Restored after an accidental overwrite during the v26 carousel sync —
   this block did not exist anywhere else, so it was rebuilt from scratch
   to match the site's existing dark/gold card-and-panel language rather
   than recovered verbatim. See README "v26 修復" note.
   ========================================================= */
.ofc-demo {
  position: relative;
  /* v27: z-index:0 here is load-bearing, not decoration. A `position:
     relative` element with `z-index: auto` does NOT establish a new
     stacking context (CSS2.1 stacking rule) — so .ofc-demo::before's
     `z-index: -1` was, this whole time, stacking against the nearest
     ancestor that DOES form a stacking context (way up the tree), not
     against .ofc-demo itself. In practice that painted the photo behind
     the page/section background, i.e. fully invisible, no matter how
     bright the image or how light the scrim — which is exactly why v22's
     "lighter scrim" and v26's "brighten the source photo" both failed to
     fix anything real. Confirmed by rendering the page with Playwright:
     adding `z-index: 0` here (creating .ofc-demo's own stacking context,
     so the pseudo-element's z-index:-1 is scoped to just behind it) is
     what actually makes the photo appear. Root-caused and fixed here for
     real — v22/v26 fixed the wrong layer. See
     how-to-play-page-task-context.md "v27". */
  z-index: 0;
  margin-top: 40px;
  border: 1px solid var(--color-border);
  border-top: 3px solid var(--color-gold);
  border-radius: var(--radius);
  /* v30: trimmed from 28px per user feedback ("互動小工具還是太高") --
     part of a wider pass tightening this widget's internal spacing; see
     .ofc-demo-topbar / .ofc-demo-footer below and
     how-to-play-page-task-context.md "v30". */
  padding: 18px 20px;
  box-shadow: var(--shadow-card);
  overflow: hidden;
  /* v30-followup3: this is a game-style interactive widget (step badge,
     card labels, explanation copy) rather than reading content, and the
     mouse-drag text-select highlight it was picking up read as a visual
     bug in screenshots. Disable text selection across the whole widget;
     the left-hand prose outside .ofc-demo is untouched and stays
     selectable as normal page text. */
  user-select: none;
  -webkit-user-select: none;
}
/* v22: the real OFC table felt photo, as a ::before layer instead of a
   background-image directly on .ofc-demo, so scrim + photo composite in
   one paint. v26: brightened the source photo itself (PIL: Brightness×2.3,
   Contrast×1.12, Color×1.15 — mean luma ~20/255 → ~44/255) and lightened
   the scrim further, since the raw photo measured too dark to read at any
   scrim opacity. Neither of those actually fixed the "looks solid black"
   report — see the z-index note on .ofc-demo above (v27) for the real
   cause. History: how-to-play-page-task-context.md "v22"/"v26"/"v27". */
.ofc-demo::before {
  content: '';
  position: absolute;
  inset: 0;
  z-index: -1;
  background-image:
    linear-gradient(165deg, rgba(18, 14, 10, 0.32), rgba(8, 8, 10, 0.48)),
    url('../assets/images/ofc-table-bg.jpg');
  background-size: cover;
  /* v30-followup: was "center 25%" -- tuned back when every step rendered
     at roughly the same (taller) height. Since v30 compacted the topbar/
     footer, step 0 (intro, no board yet) is noticeably shorter than the
     other steps, and `background-size: cover` recomputes the visible crop
     against .ofc-demo's current height -- so the same 25%-from-top anchor
     landed at a different point in the photo depending on which step was
     showing, most noticeably on the now-short step 0. Anchoring to the
     photo's own top edge instead keeps the crop's starting point fixed
     regardless of container height. */
  /* v30-followup5/6: per user feedback ("希望這個皇冠在前墩跟中墩的中間"),
     shift the crop so the crest's crown sits in the gap between the front
     and middle rows.
     v30-followup5 first tried a fixed pixel offset (`center -210px`),
     tuned against a Playwright viewport width that turned out not to
     match this page's real layout: .ofc-demo sits in a 2-column
     `.feature-split` grid inside `.container-wide` (max-width 1520px),
     so its actual rendered width on a normal desktop browser is only
     ~430-725px, not the full viewport -- confirmed wrong once the user
     screenshotted their own (much wider) browser window, where the fixed
     px offset put the crest above the front row instead of between front
     and middle.
     v30-followup6 replaces it with a PERCENTAGE anchor instead, which is
     what actually stays correct across every real desktop width: with
     `background-size: cover` scaling by WIDTH here (true for every
     desktop width down to 620px -- see the mobile override below),
     `background-position-y: P%` keeps the image's own P%-from-top point
     aligned with the container's P%-from-top point regardless of how
     much the image got scaled to reach that width. In other words the
     crop point is pinned to a fixed FRACTION of the photo, not a fixed
     pixel count, so it lands at the same relative spot in the widget
     whether the widget is 430px or 725px wide. `center 28%` was picked
     by testing candidate percentages with Playwright and eyeballing
     screenshots at 700/852/1160/1440/1728px widths (all land the crest
     consistently in the front/middle gap) -- see
     how-to-play-page-task-context.md "v30-followup6". Height still
     varies a little between steps (~641-682px, see v30-followup2), which
     nudges the crop by a few px since this formula's constant term is
     `containerHeight * P%`, but that's a small, mostly imperceptible
     drift, not the kind of "lands in a completely different spot" bug
     the original v30-followup fix was chasing. */
  background-position: center 28%;
}

/* v30: .ofc-demo-progress (step badge + restart, its own row) and
   .ofc-demo-head (round title + desc, another row below it) merged into
   one .ofc-demo-topbar -- badge and title now share a line via
   .ofc-demo-topbar-title, restart stays right-aligned. Saves a full row's
   height versus the old two-row header. See how-to-play.html and
   how-to-play-page-task-context.md "v30". */
.ofc-demo-topbar {
  display: flex;
  align-items: baseline;
  flex-wrap: wrap;
  justify-content: space-between;
  gap: 8px 14px;
  margin-bottom: 10px;
  padding-bottom: 8px;
  border-bottom: 1px solid var(--color-border);
}
.ofc-demo-topbar-title {
  display: flex;
  align-items: baseline;
  flex-wrap: wrap;
  gap: 8px 10px;
  min-width: 0;
}
.ofc-demo-topbar-title h3 {
  margin: 0;
  font-size: var(--fs-md);
}
#ofcDemoProgress {
  display: inline-block;
  font-family: ui-monospace, 'SFMono-Regular', 'Courier New', monospace;
  font-size: var(--fs-xs);
  font-weight: 700;
  letter-spacing: 0.04em;
  color: var(--color-gold-light);
  background: rgba(212, 175, 55, 0.1);
  border: 1px solid var(--color-border);
  border-left: 3px solid var(--color-gold);
  padding: 4px 10px;
  border-radius: 4px;
  white-space: nowrap;
}
.ofc-demo-restart {
  background: transparent;
  border: none;
  color: var(--color-gold-light);
  font-size: var(--fs-xs);
  font-weight: 600;
  cursor: pointer;
  padding: 4px 0;
  text-decoration: underline;
  text-underline-offset: 3px;
  white-space: nowrap;
}
.ofc-demo-restart:hover { color: var(--color-gold); }

/* v30: replaces the old .ofc-demo-head h3/p rules (that wrapper is gone —
   see how-to-play.html). #ofcDemoDesc keeps the same look, just no longer
   nested a level deep. v30-followup8: color bumped from --color-text-muted
   (dull grey-green) to --color-text per user request to brighten this
   instructional text; matches --color-text already used by .ofc-demo-result
   below, so the pre-confirm hint and the post-confirm result read at the
   same brightness instead of one being dimmer than the other. */
#ofcDemoDesc { margin: 0; font-size: var(--fs-sm); color: var(--color-text); max-width: 620px; }

/* --- Hand tray: also a drop target for "return this card to my hand" --- */
.ofc-demo-hand {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  /* v30: was 92px min-height (sized for the old 88px-tall card); trimmed
     to match the smaller card below plus its own padding, instead of
     reserving extra dead space above/below the cards. */
  min-height: 60px;
  align-items: center;
  padding: 8px 10px;
  margin-bottom: 12px;
  border: 1.5px dashed var(--color-border);
  border-radius: var(--radius-sm);
  background: rgba(0, 0, 0, 0.18);
  transition: border-color 0.15s ease, background 0.15s ease;
}
.ofc-demo-hand.can-drop,
.ofc-demo-hand.drag-over {
  border-color: var(--color-gold);
  background: rgba(212, 175, 55, 0.08);
}

/* v22: rejected-placement hint (row had room, but this round's own
   "only N cards leave the tray" quota was hit). Sits right under the hand
   tray — close to where the rejected click/drop just happened — and uses
   the same red tone as the foul feedback so it reads as "not allowed"
   without needing the full feedback-box treatment. */
.ofc-demo-hint {
  margin: -8px 0 20px;
  font-size: var(--fs-xs);
  font-weight: 600;
  color: var(--color-red-light);
}

/* --- Playing cards: deliberately skeuomorphic (cream face, red/black
   suit ink) so they read as real cards against the dark felt panel --- */
.ofc-card {
  /* v30: sized back down from v29d's 64x88 (which itself grew from 56x78
     for hit-target reasons) to 56x76 — still comfortably clickable, but
     smaller than the desktop board/hand tray needs to be to fit this
     widget's new compact height budget. Kept in lockstep with
     .ofc-demo-slot-empty below so filled and empty slots stay the same
     footprint.
     v35: rank/suit text swapped for the real card-set artwork (see
     .ofc-card-rank/.ofc-card-suit below), same asset set used on the
     game-rules pages (assets/images/cards/, from the user's
     素材/POKER/V08 material) -- the font-family/font-size rules that used
     to size the text labels are gone, replaced by fixed img heights. */
  width: 56px;
  height: 76px;
  border-radius: 8px;
  background: linear-gradient(165deg, #faf6ea, #efe6cf);
  color: #1a1a1a;
  border: 1px solid rgba(0, 0, 0, 0.15);
  box-shadow: 0 4px 10px rgba(0, 0, 0, 0.3);
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 2px;
  cursor: pointer;
  transition: transform 0.12s ease, box-shadow 0.12s ease, opacity 0.12s ease;
  flex: 0 0 auto;
}
.ofc-card:hover { transform: translateY(-3px); box-shadow: 0 8px 16px rgba(0, 0, 0, 0.35); }
.ofc-card-rank { height: 26px; display: block; }
.ofc-card-suit { height: 19px; display: block; }
.ofc-card.selected {
  outline: 3px solid var(--color-gold);
  outline-offset: 1px;
  transform: translateY(-5px);
  box-shadow: 0 10px 20px rgba(212, 175, 55, 0.35);
}
.ofc-card.preset {
  cursor: default;
  opacity: 0.6;
  filter: grayscale(0.15);
}
.ofc-card.preset:hover { transform: none; box-shadow: 0 4px 10px rgba(0, 0, 0, 0.3); }
.ofc-card[draggable="true"] { cursor: grab; }
.ofc-card.dragging { opacity: 0.35; cursor: grabbing; }

/* v30-followup: invisible same-size stand-in for a card that has left
   #ofcDemoHand (see renderHand() in the JS) -- keeps the tray's own row
   height constant as cards get placed, instead of the tray box shrinking
   down to its bare min-height once the last card or two are gone. */
.ofc-card-ghost {
  visibility: hidden;
  pointer-events: none;
  box-shadow: none;
}

/* --- The three rows: label + capacity on one side, card slots (also a
   drop target) on the other --- */
.ofc-demo-board {
  display: flex;
  flex-direction: column;
  gap: 6px;
}
.ofc-demo-row {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 7px 10px;
  border: 1px solid var(--color-border);
  border-radius: var(--radius-sm);
  background: rgba(255, 255, 255, 0.02);
  transition: border-color 0.15s ease, box-shadow 0.15s ease;
}
.ofc-demo-row-label {
  /* v30: narrowed from a 74px column (sized for two stacked lines) to a
     compact 42px tag, per user feedback referencing a mobile OFC game
     where the row label sits tight against the card row instead of
     taking its own wide column.
     v30-followup9: 42px fit every CJK label (2-3 characters) but not the
     English "Middle" (54.6px needed at this font-size/weight) -- flex-shrink
     is 0 here, so the browser let that one flex item grow past its 42px
     basis to fit the word instead of wrapping it, while Front/Back stayed
     at 42px. That made the middle row's card area start ~12px further
     right than the front/back rows' -- a real misalignment, only visible
     in English (every zh/ja label still measures <= 42px). Bumped the
     basis to 56px, just past "Middle"'s measured width, so all three
     columns render at the same width in every language instead of only
     the odd-one-out word growing. */
  flex: 0 0 56px;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 1px;
  font-weight: 700;
  color: var(--color-gold-light);
  font-size: var(--fs-sm);
  line-height: 1.2;
  text-align: center;
}
.ofc-demo-row-cap {
  font-family: ui-monospace, 'SFMono-Regular', 'Courier New', monospace;
  font-weight: 500;
  font-size: 0.68rem;
  color: var(--color-text-faint);
  letter-spacing: 0.02em;
}
.ofc-demo-row-slots {
  flex: 1;
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  /* v30: matches the new 76px .ofc-card/.ofc-demo-slot-empty height,
     was 78px in lockstep with the old 88px-tall card. */
  min-height: 76px;
  align-items: center;
  padding: 6px;
  border-radius: var(--radius-sm);
  border: 1.5px dashed transparent;
  transition: border-color 0.15s ease, background 0.15s ease;
}
/* v29c: this rule used to highlight the WHOLE .ofc-demo-row-slots
   container as one long dashed box whenever any slot in the row was
   droppable — per user feedback that read as "one big box" rather than
   "3/5/5 individual spaces". Removed; each empty slot is now its own drop
   target (see renderBoard() in the JS) and highlights itself individually
   via .ofc-demo-slot-empty.can-drop/.drag-over below. */
.ofc-demo-row.full { border-color: rgba(212, 175, 55, 0.3); }
.ofc-demo-row.result-ok {
  border-color: var(--color-gold);
  box-shadow: 0 0 0 1px rgba(212, 175, 55, 0.35);
}
.ofc-demo-row.result-foul {
  border-color: var(--color-red-light);
  box-shadow: 0 0 0 1px rgba(224, 87, 74, 0.35);
}

/* v29: fixed 3/5/5 empty slot placeholders — before this, an unfilled row
   was just an empty flex container (the "long bar" the row-slots padding/
   min-height produced), giving no visual cue for how many cards a row
   still needs. Rendered by renderBoard() to always pad each row out to
   TOTAL_MAX[row] boxes (3 for front, 5 for middle/back), card or empty. */
.ofc-demo-slot-empty {
  /* v30: kept in lockstep with .ofc-card above — see that comment. */
  width: 56px;
  height: 76px;
  border-radius: 8px;
  border: 1.5px dashed rgba(245, 241, 230, 0.16);
  background: rgba(255, 255, 255, 0.02);
  flex: 0 0 auto;
  transition: border-color 0.15s ease, background 0.15s ease;
}
/* v29c: per-slot highlight — each empty box lights up on its own (as a
   drop target for drag, or as a clickable destination once a card is
   selected) instead of the whole row's slots container turning into one
   highlighted bar. Which specific empty slot gets picked doesn't matter
   for scoring; see renderBoard()/wireDropTarget() in the JS. */
.ofc-demo-slot-empty.can-drop,
.ofc-demo-slot-empty.drag-over {
  border-color: var(--color-gold);
  border-style: solid;
  background: rgba(212, 175, 55, 0.12);
}
.ofc-demo-slot-empty.drag-over {
  background: rgba(212, 175, 55, 0.22);
}

/* v29e-followup3: replaces the old .ofc-demo-explain box below the
   board (removed) -- the per-round result now slides in over
   #ofcDemoDesc instead of occupying its own separate space, so the
   widget carries no extra reserved height for it at all. Both
   #ofcDemoDesc and #ofcDemoResult are grid children sharing the same
   grid area: CSS sizes a shared area to whichever occupant is tallest,
   so .ofc-demo-desc-wrap's height is pinned to max(desc, result) for
   this round the moment both are rendered, and never changes again for
   the rest of that round -- confirming just cross-fades/slides which
   one is visible, so the ::before background photo (background-size:
   cover, scaled against .ofc-demo's total height) never has a height
   change to react to. See renderResult()/playResultSlideIn() in
   js/how-to-play-demo.js. */
.ofc-demo-desc-wrap {
  position: relative;
  display: grid;
  margin-bottom: 14px;
}
.ofc-demo-desc-wrap > * {
  grid-area: 1 / 1;
}
#ofcDemoDesc {
  transition: opacity 0.2s ease;
}
#ofcDemoDesc.is-covered {
  opacity: 0;
}
/* v30-followup10: one invisible copy of every step's .desc string (and the
   final generic one), inserted right after #ofcDemoDesc by
   ensureDescProbes()/renderDescProbes() in js/how-to-play-demo.js. They
   share #ofcDemoDesc's font-size/max-width so each wraps exactly the same
   way the real text would at this width, and -- being .ofc-demo-desc-wrap
   grid children too -- the grid row auto-sizes to whichever one is
   tallest. That reserves the full height every step could ever need, at
   whatever width the widget currently renders at, so switching steps (or
   resizing the window) never changes #ofcDemoDesc's height and the board
   below it stops jumping between a 2-line and a 4-5-line description
   (worst in English, whose sentences run longer than the zh/ja text). */
.ofc-demo-desc-probe {
  margin: 0;
  font-size: var(--fs-sm);
  max-width: 620px;
  visibility: hidden;
  pointer-events: none;
}
.ofc-demo-result {
  margin: 0;
  font-size: var(--fs-sm);
  color: var(--color-text);
  opacity: 0;
  /* v29e-followup3: slides in from the right per user preference; bumped
     from an initial 24px/0.3s (felt too subtle in review) to 56px/0.4s so
     the motion actually reads as "sliding in" rather than a faint fade. */
  transform: translateX(56px);
  transition: opacity 0.4s ease, transform 0.4s ease;
  pointer-events: none;
}
.ofc-demo-result.is-shown {
  opacity: 1;
  transform: translateX(0);
}
.ofc-demo-result::before {
  content: '▸ ';
  color: var(--color-gold);
  font-weight: 700;
}

.ofc-demo-controls {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 8px;
}

/* v30: #ofcDemoStep5Controls (confirm/reset) and #ofcDemoNavControls
   (prev/dots/next) used to be two separate full-width .ofc-demo-controls
   rows, stacked one above the other. how-to-play.html now wraps both in
   this shared footer so they share one row instead -- confirm/reset on
   the left, nav on the right, each only as wide as its own content (see
   .ofc-demo-nav below, whose own space-between becomes a no-op once it's
   no longer stretched to the full row width). On step 0 (intro) only the
   nav side is visible; a single flex child under justify-content:
   space-between lands at flex-start, so it stays left-aligned without any
   extra rule. */
.ofc-demo-footer {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: 10px;
  margin-top: 12px;
}

/* v21: prev/dots/next nav row. Default layout spreads the three children
   (arrow / dots / arrow) across the full width; on step 0 there is no
   prev arrow and no dots (both hidden), and the shared next-button
   switches to the .ofc-demo-cta look, so .is-intro drops the
   space-between spread in favor of left alignment per the user's
   explicit "開始練習要置左" requirement. */
.ofc-demo-nav {
  align-items: center;
  justify-content: space-between;
}
.ofc-demo-nav.is-intro {
  justify-content: flex-start;
}

/* Icon-only prev/next arrows. Always visually "on" (no disabled/muted
   state) per the user's explicit "隨時都是亮的" — the only thing that
   changes their availability is the `hidden` attribute (prev on step 0,
   next on the last step), never a dimmed appearance. */
.ofc-demo-arrow {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  /* v30: was 40x40 — shrunk in step with the rest of the footer now that
     it shares a row with the confirm/reset buttons instead of getting a
     full-width row of its own. */
  width: 32px;
  height: 32px;
  border-radius: 50%;
  border: 1px solid var(--color-border);
  background: rgba(212, 175, 55, 0.08);
  color: var(--color-gold-light);
  font-size: 1.1rem;
  line-height: 1;
  cursor: pointer;
  transition: background-color 0.2s ease, border-color 0.2s ease, transform 0.15s ease;
}
.ofc-demo-arrow:hover {
  background: rgba(212, 175, 55, 0.18);
  border-color: var(--color-gold);
}
.ofc-demo-arrow:active {
  transform: scale(0.94);
}

/* On step 0 the "next" button doubles as the "開始練習" call-to-action —
   same element as the arrow (so no separate button/markup is needed), but
   restyled from a small round icon into a normal pill-shaped text button. */
.ofc-demo-arrow.ofc-demo-cta {
  width: auto;
  height: auto;
  border-radius: var(--radius-sm);
  padding: 9px 22px;
  font-size: var(--fs-sm);
  font-family: var(--font-body);
  font-weight: 600;
  background: var(--color-gold);
  color: var(--color-bg);
  border-color: var(--color-gold);
  /* v30-followup7: per user feedback (two screenshots of the widget's
     bottom edge, "不要點了開始練習後底圖往下長一點點") -- .ofc-demo-footer's
     height is set by its tallest child, and this CTA button (step 0's
     only visible footer content) rendered ~5.3px shorter than the
     confirm/reset .btn.btn-sm buttons that take over the footer from
     step 1 onward (different padding/font-size, close but not equal).
     That made the whole widget ~5px shorter on step 0 than every other
     step, so clicking "開始練習" visibly grew the widget by that amount.
     Pinning this button's height to match .btn-sm's actual rendered
     height (measured via Playwright, see
     how-to-play-page-task-context.md "v30-followup7") keeps the footer
     -- and so the whole widget -- the same height on step 0 as on every
     later step. */
  min-height: 41px;
}
.ofc-demo-arrow.ofc-demo-cta:hover {
  background: var(--color-gold-light);
  border-color: var(--color-gold-light);
}

/* Simple dot-progress pagination (●●○○○○) — the style the user explicitly
   picked over a numbered/ellipsis pattern. */
.ofc-demo-dots {
  display: flex;
  align-items: center;
  gap: 6px;
}
.ofc-demo-dot {
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: rgba(245, 241, 230, 0.18);
  transition: background-color 0.2s ease, transform 0.2s ease;
}
.ofc-demo-dot.active {
  background: var(--color-gold);
  transform: scale(1.25);
}

/* Elements inside .ofc-demo set their own `display` (flex, in several
   cases), which otherwise beats the browser's default `[hidden] {
   display: none }` UA rule (an author declaration wins over a UA one even
   at equal specificity). Concretely: `.ofc-demo-controls { display: flex }`
   meant `roundControls.hidden = true` at Step 0 (no round yet) had no
   visible effect, so Confirm/Reset/Show-a-Foul stayed on screen before any
   cards were even dealt — this one rule (with !important so it always
   wins, regardless of what future .ofc-demo-* rules set `display` to)
   covers that container, the hand tray, and the individual buttons alike. */
.ofc-demo [hidden] { display: none !important; }

/* v29e-followup5: the final round's pass/fail result used to render into
   its own #ofcDemoFeedback box below the board (.ofc-demo-feedback /
   .success / .foul, now removed) -- a whole extra block, always taking up
   space of its own. Per user feedback that this made the widget too tall
   to comfortably use, the final round's result now shares the exact same
   #ofcDemoResult overlay (sliding in over #ofcDemoDesc under the title)
   that rounds 1-4 already used -- see renderResult() / buildFinalResultHtml()
   in js/how-to-play-demo.js. The pass/fail badge below carries the color
   (red for a foul) instead of a separate tinted box. */

/* v29b: 成功/失敗 badge for rounds 1-4, rendered inline at the very front
   of the round result text (by renderResult() in the JS -- see
   .ofc-demo-result in the block above) instead of a separate pinned box
   on .ofc-demo-feedback -- per user feedback that a standalone badge box
   felt like one extra element for what reads as a single message. Small
   rounded chip, sized to sit next to the "▸" prefix rather than compete
   with it. v29e-followup5: also now used by the final round's pass/fail
   result (see .fail below getting reused for "犯規了！"). */
.ofc-demo-result-badge {
  display: inline-block;
  margin-right: 8px;
  padding: 2px 10px;
  border-radius: 999px;
  font-size: var(--fs-xs);
  font-weight: 700;
  letter-spacing: 0.04em;
  vertical-align: 1px;
}
.ofc-demo-result-badge.ok {
  background: linear-gradient(135deg, #5fc487, #2e8b57);
  color: #eafff1;
}
.ofc-demo-result-badge.fail {
  background: linear-gradient(135deg, var(--color-red-light), var(--color-red));
  color: #fff0ee;
}

/* v29: FLIP-style "moved" glow — playMoveAnimation() in the JS slides a
   card from its old on-screen position to its corrected slot, then adds
   this class for the duration of that transition to draw the eye to
   which cards actually moved. Removed again once the transition ends. */
.ofc-card.card-move-glow {
  box-shadow: 0 0 0 3px rgba(212, 175, 55, 0.55), 0 6px 16px rgba(212, 175, 55, 0.4);
}

/* v21: foul reveal, overlaid centered on the widget. Hidden by default
   (opacity 0, no pointer events); JS toggles .boom-play (with a forced
   reflow in between so re-triggering after a reset restarts the
   animation instead of no-op'ing because the class was already present).
   v29e-followup4: was a plain "💥" emoji text node directly inside this
   div; now an <img> (assets/images/foul-grenade.png, a real grenade
   stamped "FOULED" from the site's own art library) sits inside instead.
   An <img> can't be the flex-centering container AND the thing being
   scaled/rotated at once, so the animation is split: this outer div still
   just fades opacity in/out, and the scale/rotate part of the original
   single keyframe moved to the img child below. */
.ofc-demo-boom {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  opacity: 0;
  pointer-events: none;
  z-index: 5;
}
.ofc-demo-boom img {
  width: 132px;
  height: auto;
  filter: drop-shadow(0 14px 26px rgba(0, 0, 0, 0.6));
}
.ofc-demo-boom.boom-play {
  animation: ofcBoomFade 0.9s ease-out;
}
.ofc-demo-boom.boom-play img {
  animation: ofcBoomPlay 0.9s ease-out;
}
@keyframes ofcBoomFade {
  0% { opacity: 0; }
  30% { opacity: 1; }
  75% { opacity: 1; }
  100% { opacity: 0; }
}
@keyframes ofcBoomPlay {
  0% { transform: scale(0.3) rotate(-8deg); }
  30% { transform: scale(1.3) rotate(4deg); }
  50% { transform: scale(1) rotate(-2deg); }
  75% { transform: scale(1.08) rotate(0deg); }
  100% { transform: scale(1.35) rotate(0deg); }
}

/* v29e-followup5: turns the grenade reveal into an actual explosion —
   per user feedback ("手榴彈特效可以是爆炸效果嗎？"), the grenade image
   itself stays, but a radial flash and a few flying debris bits (both
   pure CSS, no extra art) now play behind/around it, plus a short shake
   on #ofcDemo itself below. Both are positioned absolutely so they don't
   participate in .ofc-demo-boom's flex-centering of the <img>. */
.ofc-demo-boom-flash {
  position: absolute;
  width: 220px;
  height: 220px;
  border-radius: 50%;
  background: radial-gradient(circle, rgba(255, 214, 120, 0.95) 0%, rgba(224, 87, 74, 0.55) 42%, rgba(224, 87, 74, 0) 72%);
  opacity: 0;
  transform: scale(0.2);
  pointer-events: none;
}
.ofc-demo-boom.boom-play .ofc-demo-boom-flash {
  animation: ofcBoomFlash 0.6s ease-out;
}
@keyframes ofcBoomFlash {
  0% { opacity: 0; transform: scale(0.2); }
  18% { opacity: 1; transform: scale(1); }
  55% { opacity: 0.45; transform: scale(1.15); }
  100% { opacity: 0; transform: scale(1.3); }
}

.ofc-demo-boom-debris {
  position: absolute;
  inset: 0;
  pointer-events: none;
}
.ofc-demo-boom-debris span {
  position: absolute;
  top: 50%;
  left: 50%;
  width: 9px;
  height: 9px;
  border-radius: 1px;
  background: var(--color-gold);
  box-shadow: 0 0 6px 1px rgba(255, 200, 110, 0.7);
  opacity: 0;
}
/* Each span gets its own outward direction (--dx/--dy) and a slight
   timing stagger so the debris doesn't read as one uniform ring. */
.ofc-demo-boom-debris span:nth-child(1) { --dx: -74px; --dy: -46px; }
.ofc-demo-boom-debris span:nth-child(2) { --dx: 68px; --dy: -56px; background: var(--color-red-light); }
.ofc-demo-boom-debris span:nth-child(3) { --dx: -86px; --dy: 32px; animation-delay: 0.02s; }
.ofc-demo-boom-debris span:nth-child(4) { --dx: 80px; --dy: 40px; background: var(--color-red-light); animation-delay: 0.03s; }
.ofc-demo-boom-debris span:nth-child(5) { --dx: -28px; --dy: -82px; animation-delay: 0.05s; }
.ofc-demo-boom-debris span:nth-child(6) { --dx: 34px; --dy: 80px; background: var(--color-red-light); animation-delay: 0.04s; }
.ofc-demo-boom.boom-play .ofc-demo-boom-debris span {
  animation-name: ofcBoomDebris;
  animation-duration: 0.7s;
  animation-timing-function: ease-out;
  animation-fill-mode: forwards;
}
@keyframes ofcBoomDebris {
  0% { opacity: 0; transform: translate(-50%, -50%) translate(0, 0) scale(1); }
  15% { opacity: 1; }
  100% { opacity: 0; transform: translate(-50%, -50%) translate(var(--dx), var(--dy)) scale(0.4); }
}

/* Short shake on the whole widget, toggled alongside .boom-play by
   triggerBoom() in the JS (root.classList — root IS #ofcDemo). Small
   amplitude, translate-only so it moves .ofc-demo's own background photo
   along with it rather than revealing a seam. */
.ofc-demo-shake {
  animation: ofcBoomShake 0.5s ease-out;
}
@keyframes ofcBoomShake {
  0%, 100% { transform: translate(0, 0); }
  15% { transform: translate(-6px, 2px); }
  30% { transform: translate(5px, -2px); }
  45% { transform: translate(-4px, 1px); }
  60% { transform: translate(3px, -1px); }
  75% { transform: translate(-2px, 1px); }
  90% { transform: translate(1px, 0); }
}

@media (max-width: 620px) {
  /* v30-followup5/6: reset the desktop-only crown shift (`center 28%`,
     see .ofc-demo::before above) back to the original "center top"
     anchor. Below this width `background-size: cover` scales the
     portrait source photo almost exactly to the container's height (see
     the comment on .ofc-demo::before), leaving barely any vertical slack
     to shift -- applying the desktop offset here just pushes the crest
     out of frame instead of repositioning it. Per user feedback this
     round ("手機版之後還要調整，先處理電腦版"), mobile is intentionally
     left as-is for now and will get its own pass later. */
  .ofc-demo::before { background-position: center top; }
  /* v29e-followup7: second round of "make it shorter" per user feedback
     ("互動小工具還是太高，整體再縮小一點") on top of the v29e-followup5
     board-height bug fix -- this time it's not a bug, just trimming
     padding/margins/gaps that were sized for the roomier desktop layout
     down to something tighter for a single mobile column, plus a smaller
     .ofc-demo-board row gap and a shorter card aspect-ratio (see below).
     None of these numbers are load-bearing for anything else on mobile
     (.ofc-demo-row-label / .ofc-card sizing above stays fluid/unchanged),
     so they're safe to just turn down. */
  .ofc-demo { padding: 14px; }
  .ofc-demo-row { flex-direction: column; align-items: stretch; gap: 6px; padding: 10px; }
  .ofc-demo-board { gap: 8px; }
  .ofc-demo-row-slots { padding: 4px; }
  .ofc-demo-hand { padding: 10px; margin-bottom: 12px; }
  /* v30: .ofc-demo-controls no longer carries margin-top itself (see the
     base rule) now that confirm/reset and prev/dots/next share one
     .ofc-demo-footer row -- this replaces the old rule that set it here. */
  .ofc-demo-footer { margin-top: 10px; }
  /* v29e-followup5: `.ofc-demo-row-label`'s desktop rule sets
     `flex: 0 0 74px` to fix its WIDTH as a narrow side column next to the
     row's slots (side-by-side layout). Here `.ofc-demo-row` switches to
     `flex-direction: column`, which flips the flex main axis to vertical —
     so that same `0 0 74px` silently became a fixed HEIGHT instead,
     reserving ~74px for a label that only needs about a single line
     (~24px), on every one of the 3 rows. That's roughly 150px of pure
     dead space stacked into the board on every mobile screen — far and
     away the single biggest chunk of the widget's on-phone height per
     user feedback ("整體互動小工具...在網頁上很難操作"), confirmed via
     Playwright measurement (.ofc-demo-board == 808px at 390px viewport,
     of which each row's label alone was 74px instead of ~24px). Resetting
     the flex-basis to `auto` here lets it size to its actual content. */
  .ofc-demo-row-label { flex: 0 0 auto; flex-direction: row; align-items: baseline; justify-content: space-between; }
  /* v29e-followup5: per user feedback ("在手機上看前、中、後墩不能變成兩行"),
     中墩/後墩 (5 cards each) must never wrap onto a second line on phones.
     A first attempt just picked a smaller fixed px card size for this
     breakpoint, sized off Playwright measurements at a couple of test
     widths -- but that's exactly the kind of fixed number that breaks
     again on the next narrower phone (or if `.ofc-demo-row-slots`'s own
     padding below is ever changed). Fluid sizing instead: each card's
     width is *defined* as "however wide 5 of them + 4 gaps make one line
     of `.ofc-demo-row-slots`'s own content box", via `calc()` against 100%
     (a flex item's percentage width resolves against its flex container's
     content box -- i.e. row-slots' width minus row-slots' own 6px*2
     padding, see .ofc-demo-row-slots above). That makes 5-across
     one-line-ness true by construction at any viewport width, not just
     the ones tested. `aspect-ratio` keeps the card's proportions instead
     of a second hand-picked height number. */
  .ofc-card, .ofc-demo-slot-empty {
    width: calc((100% - 4 * 6px) / 5);
    height: auto;
    /* v29e-followup7: shorter than the desktop 54/76 ratio (was kept in
       lockstep with it) -- per user feedback the rank/suit text should
       fill more of the card face, and a squarer card both leaves more
       vertical room for a bigger font (below) and trims a further slice
       off .ofc-demo-board's height, on top of the v29e-followup5 fix. */
    aspect-ratio: 54 / 64;
  }
  .ofc-demo-row-slots { gap: 6px; }
  /* v29e-followup6: the width formula above assumes a 6px gap between
     items (the "4 * 6px" term) -- correct for `.ofc-demo-row-slots`
     right above, but `#ofcDemoHand` (the dealt-but-not-yet-placed hand
     tray, up to 5 cards) still had its base 10px gap, which the formula
     didn't know about. That mismatch meant each hand card computed ~3-4px
     wider than the tray actually had room for, so 5 cards still wrapped
     to two lines there even though the board rows below were correctly
     fixed -- confirmed by testing the hand tray specifically instead of
     assuming the unscoped `.ofc-card` rule covered every container it
     applies to. Matching the gap here makes the same "divide by 5" formula
     correct for the hand tray too. */
  .ofc-demo-hand { gap: 6px; }
  /* Font size still needs a number, but a fluid one instead of a step
     per breakpoint: scale with viewport width (a reasonable proxy for
     the card's own width here, since `.ofc-demo-row` is always a single
     full-width column on mobile), clamped so it never gets unreadably
     small on a narrow phone or oversized once .ofc-demo hits its 620px
     upper bound. */
  /* v29e-followup7: bumped up per user feedback ("撲克牌的數字跟花色可以
     佔整張撲克牌比多一點") -- larger clamp values at both ends so the
     rank/suit text reads as filling the card face instead of floating in
     the middle of it, paired with the shorter aspect-ratio above and the
     tighter .ofc-card gap below. */
  .ofc-card { gap: 1px; }
  /* v35: the images scale with the card's own fluid width (the aspect-ratio
     box above) via percentage sizing, replacing the old clamp()'d
     font-size pair -- same ratio the desktop fixed px heights use
     (26/56 and 19/56 of the 56px desktop card). */
  .ofc-card-rank { width: 46%; height: auto; }
  .ofc-card-suit { width: 34%; height: auto; }
}

/* --- Full-bleed "feature band" (v9/v10) ---
   Full-bleed background (edge to edge, independent of .container's
   1160px cap) so a block reads as a distinct featured banner —
   referencing the user-cited playwsop.com/strategies/ top block —
   rather than sitting inside the same narrow column as the rest of the
   page. Paired with .container-wide (1520px) on the content inside it.
   v9 introduced this for the OFC core-rules block only (as
   .section-ofc-feature); v10 renamed it to the generic .section-feature
   and reused it for the Scoring and Fantasyland sections too. */
.section-feature {
  position: relative;
  padding-top: 40px;
  padding-bottom: 64px;
  background: linear-gradient(180deg, rgba(212, 175, 55, 0.07), rgba(212, 175, 55, 0.02) 45%, transparent);
  border-top: 1px solid var(--color-border);
  border-bottom: 1px solid var(--color-border);
}

/* --- Generic left-text / right-media split (v9 → v11) ---
   Originally built for the OFC core-rules block (as .ofc-feature) with
   the interactive demo on the right; v11 reuses it, renamed generically,
   for the Scoring section (table on the right) and Fantasyland (an image
   placeholder on the right, until the user supplies real art). Two
   columns on desktop, stacks to one column on narrow screens. See
   how-to-play-page-task-context.md "v9"/"v10"/"v11" for the discussion. */
.feature-split {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 1.05fr);
  gap: 56px;
  align-items: start;
  margin-top: 28px;
}
.feature-split-text > p {
  max-width: 560px;
  font-size: var(--fs-md);
  color: var(--color-text);
}
.feature-split-media .ofc-demo { margin-top: 0; }

.ofc-more { margin-top: 20px; }
.ofc-more > p { max-width: 560px; color: var(--color-text); }

.ofc-mini-steps {
  list-style: none;
  margin: 20px 0 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 16px;
}
.ofc-mini-steps li {
  padding-top: 16px;
  border-top: 1px solid var(--color-border);
}
.ofc-mini-steps li:first-child { padding-top: 0; border-top: none; }
.ofc-mini-steps strong {
  display: block;
  margin-bottom: 6px;
  font-family: var(--font-heading);
  color: var(--color-gold-light);
  font-size: var(--fs-base);
}
.ofc-mini-steps p {
  margin: 0;
  font-size: var(--fs-sm);
  max-width: 560px;
  color: var(--color-text);
}

/* v23: v16 added .sng-copy to brighten SNG's <p> color while it was still
   a plain single-column .section-alt block (not wrapped in
   .feature-split-text). v23 moved SNG into the same .feature-split
   layout as OFC/Scoring/Fantasyland, so .feature-split-text > p already
   covers this color and the .sng-copy class/rule were removed as
   unused — no page still references .sng-copy. */

/* v25: the Scoring royalty-table's .feature-split-media column got a
   dedicated .scoring-table-media class instead of the generic selector
   that used to sit here (`.feature-split-media .royalty-table`), so this
   section's width/font/position tweaks below don't leak onto any other
   .feature-split-media column (none currently hold a table, but future
   sections might). See .scoring-table-media further down for the
   replacement rules and the reasoning behind them. */

/* v23: real art supplied for both Fantasyland and SNG (actual in-app
   tutorial/gameplay screenshots, cropped from 素材/舊screenshot/) —
   .feature-placeholder is now unused (removed) and both sections'
   .feature-split-media columns hold a plain <img class="feature-split-img">
   instead. Framed like a captured screenshot — rounded corners, border,
   drop shadow — matching the site's existing card language rather than a
   bare, edge-to-edge image. */
.feature-split-img {
  display: block;
  width: 100%;
  border-radius: var(--radius);
  border: 1px solid var(--color-border);
  box-shadow: var(--shadow-card);
}

/* User feedback (2026-09-21): the raw source images for Fantasyland
   (landscape, 900x469) and SNG (portrait, 900x1268) have wildly
   different aspect ratios, so at width:100% they rendered at very
   different natural heights next to their paired text column -- SNG's
   photo towered far past its text (portrait crop scaled to column
   width comes out ~1.4x taller than wide), while Fantasyland's short
   landscape crop left a lot of empty space below it. Capping both to
   the same fixed height + object-fit:cover makes each image's height
   track its neighboring text block instead of its own raw aspect
   ratio. object-position is tuned per image: SNG keeps the top of the
   frame (trophy, both hands' AAAQQ, player nameplates) since that's
   the part v23 cropped the screenshot for; Fantasyland stays centered
   since the "FANTASYLAND" watermark and card row are already centered
   in the source crop. */
.feature-split-img--sng,
.feature-split-img--fantasy {
  object-fit: cover;
}
.feature-split-img--fantasy {
  height: 420px;
  object-position: center;
}
/* v29: SNG image re-cropped (now ~1:1 instead of the old tall 900x1268
   portrait) to include the third player's hand, which the old crop's
   shared 420px height was cutting off. Given its own height instead of
   sharing Fantasyland's -- sized to the image's natural aspect ratio at
   its actual rendered column width (~602px desktop / ~342px mobile,
   measured with Playwright), so object-fit:cover has nothing left to
   crop and the full three-hand screenshot shows. */
.feature-split-img--sng {
  /* v29b: a fixed px height (originally 630, tuned for this column's
     width at 1280px viewport) turned out fragile: .container-wide (this
     section's container) has no max-width until 1520px, so on a wider
     browser window the column keeps growing past 602px up to ~725px,
     and object-fit:cover with a height that no longer matches cropped
     off the third hand again (reported by user on an actual wide
     screen, not just my 1280px test). aspect-ratio locks the box to the
     source image's own ratio at any column width, so cover never has
     anything to crop vertically -- verified with Playwright at 960 /
     1280 / 1520 / 1800px widths, full three-hand image visible at all
     of them.
     v29e: source image itself re-cropped (1284x1340 -> 1284x1185) per
     user feedback -- the old full frame had a lot of dead empty-slot
     felt above the first row of revealed cards, and ended with a stray,
     disconnected bit of finger/knuckle hanging past the bottom-right
     card with nothing else of the hand in frame. New crop trims most of
     that empty top margin and cuts the bottom right where the third
     player's AAA row ends, so only the natural fingertip-on-card touch
     point remains (no more stray stub below it).
     v29e-followup: v29e's bottom cut turned out too tight -- user asked
     for more of the finger to stay visible, not just a bare touch point.
     Re-cropped again from the same pristine 1284x1340 original, keeping
     the same top trim (dead felt removed) but this time keeping the
     bottom edge as-is (1284x1210), so the whole finger from knuckle to
     fingertip reads naturally instead of being cut mid-nail.
     aspect-ratio updated to match the new image's exact ratio, same
     invariant as v29b/v29e: cover has nothing left to crop, so the
     whole (re-cropped) image always shows, no matter the column's
     rendered width. */
  aspect-ratio: 1284 / 1210;
  object-position: top;
}

@media (max-width: 900px) {
  .feature-split-img--fantasy { height: 320px; }
}

/* v25: user feedback was that the full-width royalty-table looked
   sparse/empty (a lot of horizontal whitespace across just 3 narrow
   columns) and that its top edge didn't line up with this section's own
   <h2> above the grid — the grid (.feature-split, align-items:start)
   only starts below the eyebrow + h2 + their bottom margins, so the
   table's top sat noticeably lower than the heading text. Fixed both:
   capped the table back down to a comfortable reading width (was
   max-width:none/100% before), bumped the cell font size up a notch now
   that there's less width to fill, and pulled this column up with a
   negative margin.
   v28: the -90px from v25 was a calculated estimate (this page's file://
   path couldn't be opened in a browser at the time to check), and it
   overshot — user feedback ("這裡怪怪的") plus a Playwright render showed
   the table's top actually landing ~16px ABOVE the h2's top, not level
   with it. Measured the real gap with Playwright bounding boxes
   (h2.top vs .scoring-table-media.top) instead of recalculating from CSS
   values, and corrected to -74px, which lines the two up exactly at
   1280px width. */
.scoring-table-media { margin-top: -74px; }

/* v29: user feedback was that Fantasyland's image sat noticeably lower
   than the section's own h2 (same misalignment .scoring-table-media
   fixed above for the royalty table) -- same fix, same cause
   (.feature-split's align-items:start only starts below the eyebrow +
   h2), same measured value (-74px lines the image top up exactly with
   the h2 top at 1280px width, verified with Playwright bounding_box()). */
.fantasy-media { margin-top: -74px; }
.sng-media { margin-top: -74px; }
.ofc-demo-media { margin-top: -74px; }
.scoring-table-media .royalty-table { max-width: 440px; width: 100%; }
.scoring-table-media .royalty-table th,
.scoring-table-media .royalty-table td { font-size: var(--fs-md); }

@media (max-width: 900px) {
  .feature-split { grid-template-columns: 1fr; gap: 28px; }
}

/* --- Page-wide type bump for how-to-play.html only (v10) ---
   Scoped to the .page-how-to-play body class rather than the global
   :root tokens, so it doesn't change font sizes on any other page.
   Overrides the shared --fs-* scale (used by labels/captions/small text
   across the site) plus the body base size, which most plain <p> tags
   on this page inherit since they don't set their own font-size. */
.page-how-to-play {
  font-size: 17px;
  --fs-xs: 0.86rem;
  --fs-sm: 0.98rem;
  --fs-base: 1.08rem;
  --fs-md: 1.16rem;
  --fs-lg: 1.24rem;
}
/* v31: the v10 type bump above cascades to every descendant of <body>,
   including .site-header/.site-footer -- which aren't this page's own
   copy, they're the same shared nav/footer markup every other page uses
   and are supposed to render identically everywhere. Because this page's
   <nav> sits inside <body class="page-how-to-play">, it was silently
   inheriting the ~6-8% bump (16px->17px, --fs-* all up a notch), making
   the logo/links/button look visibly bigger here than on index/about/
   faq/game-rules -- reported by the user as the nav "放大"
   (enlarging) when navigating onto this page. Resetting both chrome
   regions back to the site's base scale keeps the v10 bump scoped to
   this page's own <main> content, which is what it was meant for. */
.page-how-to-play .site-header,
.page-how-to-play .site-footer {
  font-size: 16px;
  --fs-xs: 0.8rem;
  --fs-sm: 0.92rem;
  --fs-base: 1rem;
  --fs-md: 1.08rem;
  --fs-lg: 1.15rem;
}

/* v16: the v15 eyebrow copy grew from 2-4 character tags into full HQ
   briefing phrases ("【HQ 訓令 // OFC 基礎協議】"), and the letter-spacing
   tuned for short tags made those long phrases feel cramped and harder to
   scan. Loosen the spacing and bump the size slightly, scoped to this page
   only — other pages still use the original short-tag styling. */
.page-how-to-play .eyebrow {
  letter-spacing: 0.07em;
  font-size: calc(var(--fs-xs) * 1.15);
}

/* ==========================================================================
   game-rules.html (v8) — shared tab-switcher rules page for NLH / PLO /
   7 Card Stud / Blackjack / Rummy / Gin Rummy. Content is rendered by the
   page's own inline script from a bilingual (en / zh-Hant) data object;
   these are purely the presentational classes it builds elements with.
   Deliberately restyled to the site's dark + gold theme rather than the
   felt-green prototype these rules were drafted against — see
   how-to-play-page-task-context.md "v8" for the reasoning.
   ========================================================================== */

.gr-back-link {
  display: inline-block;
  margin-bottom: 18px;
  font-size: var(--fs-sm);
  color: var(--color-text-faint);
  transition: color 0.15s ease;
}
.gr-back-link:hover { color: var(--color-gold-light); }

/* per-game hero banner (v27 demo) — the "各遊戲牌桌" background photo for
   the currently selected game, with the real game title (e.g. "No Limit
   Hold'em") overlaid; this text doesn't appear anywhere else on the page
   since the panel's first content block is a generic "How to Play" h2. */
.gr-hero-banner {
  position: relative;
  height: 220px;
  border-radius: var(--radius-md, 12px);
  overflow: hidden;
  margin-bottom: 32px;
  background-size: cover;
  background-position: center;
  border: 1px solid var(--color-border);
}
.gr-hero-banner::before {
  content: "";
  position: absolute;
  inset: 0;
  background: linear-gradient(180deg, rgba(10, 8, 6, 0.1) 0%, rgba(10, 8, 6, 0.5) 55%, rgba(10, 8, 6, 0.92) 100%);
}
.gr-hero-banner-inner {
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  padding: 18px 26px 20px;
  z-index: 1;
}
.gr-hero-banner-tag {
  display: block;
  font-family: ui-monospace, 'SFMono-Regular', 'Courier New', monospace;
  font-size: var(--fs-xs);
  font-weight: 600;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--color-gold-light);
  margin-bottom: 4px;
}
.gr-hero-banner h2 {
  margin: 0;
  font-size: clamp(1.5rem, 3vw, 2.1rem);
  color: var(--color-text);
}
.gr-hero-banner-logo {
  display: block;
  height: clamp(34px, 5vw, 52px);
  width: auto;
}
/* same reasoning as .feature-card-logo--stud on how-to-play.html: the
   "7 Card Stud" wordmark is a thinner mixed-case script, not the bold
   all-caps sans the other five titles use, so it reads smaller/lighter
   at the same box height here too. */
.gr-hero-banner-logo--stud {
  height: clamp(40px, 6vw, 62px);
}
@media (max-width: 640px) {
  .gr-hero-banner { height: 150px; }
  .gr-hero-banner h2 { font-size: 1.3rem; }
  .gr-hero-banner-logo { height: 30px; }
  .gr-hero-banner-logo--stud { height: 36px; }
}

.gr-switch {
  position: sticky;
  top: 68px;
  z-index: 90;
  background: var(--color-bg);
  border-bottom: 1px solid var(--color-border);
  padding-block: 12px;
  margin-bottom: 40px;
}
.gr-switch-inner {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
}
.gr-tab {
  font-family: var(--font-heading);
  font-size: var(--fs-sm);
  font-weight: 600;
  color: var(--color-text-muted);
  background: transparent;
  border: 1px solid var(--color-border);
  border-radius: var(--radius-sm);
  padding: 8px 16px;
  cursor: pointer;
  transition: color 0.15s ease, background-color 0.15s ease, border-color 0.15s ease;
}
/* game-rules static pages (game-rules-static-plan.md) use <a class="gr-tab">
   page links instead of the old JS <button role="tab">, so this needs the
   anchor reset + the "current page" look driven by aria-current instead of
   aria-selected. aria-selected is kept too for as long as the legacy
   dynamic game-rules.html (?game=) still renders JS <button> tabs. */
a.gr-tab { display: inline-block; text-decoration: none; }
.gr-tab:hover { color: var(--color-text); border-color: var(--color-gold); }
.gr-tab[aria-selected="true"],
.gr-tab[aria-current="page"] {
  color: var(--color-bg);
  background: linear-gradient(135deg, var(--color-gold-light), var(--color-gold));
  border-color: var(--color-gold);
}
.gr-tab:focus-visible { outline: 2px solid var(--color-gold); outline-offset: 2px; }

/* v30 -- the whole rule-text column (divider/h2, h3, sub-labels, p,
   vrows/paytable, ranking table) is centered as a single 900px reading
   column inside the 1160px .container, instead of hugging the left
   edge with empty space on the right. Widened from the v29 720px cap
   per the user's request (compare Pokerrrr's About page, which reads
   left-aligned but fills more of its container). */
.gr-panel .gr-sec-head {
  border-top: 2px solid var(--color-gold);
  padding-top: 10px;
  margin: 44px 0 18px;
  max-width: 900px;
}
.gr-panel .gr-sec-head:first-child { margin-top: 0; }
.gr-panel .gr-sec-head h2 { margin: 0; }

.gr-panel h3 {
  font-family: ui-monospace, 'SFMono-Regular', 'Courier New', monospace;
  font-size: var(--fs-xs);
  font-weight: 600;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--color-gold-light);
  margin: 30px 0 10px;
  max-width: 900px;
}

/* explanatory rule text reads as the primary content of this page,
   not secondary copy -- override the site-wide muted-gray <p> color
   with full text color for legibility. */
.gr-panel p { max-width: 900px; white-space: pre-line; color: var(--color-text); }
.gr-sub { font-weight: 600; color: var(--color-text); margin-bottom: 6px; max-width: 900px; }

.gr-ph {
  font-family: ui-monospace, 'SFMono-Regular', 'Courier New', monospace;
  font-size: 0.9em;
  border-bottom: 1px dotted var(--color-text-faint);
  color: var(--color-text-faint);
}

/* step list (Rummy's Undo / All button explainer) */
.gr-steps { display: flex; flex-direction: column; margin-bottom: 20px; max-width: 900px; }
.gr-step {
  display: grid;
  grid-template-columns: 8rem 1fr;
  gap: 4px 20px;
  padding: 12px 0;
  border-bottom: 1px solid var(--color-border);
  align-items: baseline;
}
.gr-step-name {
  font-family: ui-monospace, 'SFMono-Regular', 'Courier New', monospace;
  font-size: var(--fs-sm);
  color: var(--color-gold-light);
}
.gr-step-body { margin: 0; max-width: 32rem; }
.gr-step-body + .gr-step-body { margin-top: 4px; }

/* playing cards — same skeuomorphic cream-face treatment as .ofc-card,
   just smaller so a full paytable row still fits comfortably */
.gr-hand { display: flex; flex-wrap: wrap; gap: 6px; align-items: center; margin: 0 0 18px; }
/* a hand made of several card groups (e.g. a Rummy "before -> after" demo,
   or several independent example hands shown side by side) renders each
   run of cards in its own bordered box instead of all cards loose in one
   row, so it reads as distinct groups rather than one jumbled hand. */
.gr-hand.is-grouped { gap: 10px; }
.gr-hand-group { display: flex; flex-wrap: wrap; gap: 6px; padding: 6px; border: 1px solid var(--color-border); border-radius: 8px; background: rgba(255, 255, 255, 0.03); }
.gr-hand-arrow { display: flex; align-items: center; font-size: 1.3rem; color: var(--color-gold); padding: 0 2px; }
.gr-hand-gap { display: inline-block; width: 14px; }
/* a standalone hand-of-cards example (Rummy/Gin Rummy) rendered directly into the
   panel needs the same 900px-centered treatment as the surrounding text, otherwise
   it juts out past the left edge of the centered column above/below it. Scoped to a
   direct child of #grPanel so it does not touch .gr-hand nested inside .gr-vrow/
   .gr-prow/.gr-rank-table, which have their own margin resets right above. */
.gr-panel > .gr-hand { max-width: 900px; }
.gr-card {
  width: 40px;
  height: 56px;
  border-radius: 6px;
  background: linear-gradient(165deg, #faf6ea, #efe6cf);
  color: #1a1a1a;
  border: 1px solid rgba(0, 0, 0, 0.15);
  box-shadow: 0 3px 8px rgba(0, 0, 0, 0.3);
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 1px;
  flex: 0 0 auto;
}
/* v35: rank/suit are now the real card-set artwork (assets/images/cards/,
   sourced from the user's 素材/POKER/V08 material) instead of CSS text --
   see .gr-card-rank/.gr-card-suit below. The images are pre-colored
   (black/red per suit) so .is-red no longer needs to set a text color, but
   the class is still applied for anything else that might key off it. */
.gr-card-rank { height: 18px; display: block; }
.gr-card-suit { height: 13px; display: block; }
.gr-card.joker { padding: 4px; }
.gr-card.joker img { width: 100%; height: 100%; display: block; object-fit: contain; }
.gr-card.back {
  background-image: url("../assets/images/cards/card-back.png");
  background-size: cover;
  background-position: center;
  border-color: var(--color-border);
}
.gr-card.gap { background: transparent; border: 0; box-shadow: none; }

/* value / paytable rows -- capped to the same 720px reading width as
   .gr-panel p, otherwise the 1fr/auto grid columns stretch across the
   full container and blow the label/amount gap wide open on desktop. */
.gr-vrows {
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-sm);
  padding: 18px;
  margin: 0 0 24px;
  max-width: 900px;
  display: flex;
  flex-direction: column;
  gap: 14px;
}
.gr-vrow { display: grid; grid-template-columns: 1fr auto; align-items: center; gap: 8px 16px; }
.gr-vrow .gr-hand, .gr-prow .gr-hand { margin: 0; }
.gr-amount {
  font-family: ui-monospace, 'SFMono-Regular', 'Courier New', monospace;
  font-size: var(--fs-sm);
  color: var(--color-gold-light);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}
.gr-prow { display: grid; grid-template-columns: minmax(7.5rem, max-content) 1fr auto; align-items: center; gap: 6px 16px; }
.gr-kind {
  font-family: ui-monospace, 'SFMono-Regular', 'Courier New', monospace;
  font-size: var(--fs-sm);
  letter-spacing: 0.04em;
  color: var(--color-text);
}
/* The payout label column above is sized (and letter-spaced) for short
   English words like "Push"/"Bust"; Chinese/Japanese labels such as
   "プッシュ（引き分け）" or "ブラックジャック" are longer and don't carry
   Latin-style tracking well, so they wrapped mid-word inside the fixed
   7.5rem column with extra-looking gaps. minmax(...,max-content) above lets
   the column grow to fit, and :lang() here drops the monospace tracking
   for CJK text specifically (English labels are untouched). */
:lang(zh-Hant) .gr-kind, :lang(ja) .gr-kind {
  font-family: var(--font-body);
  letter-spacing: normal;
}

/* shared poker-hand ranking table (NLH / PLO / 7 Card Stud) */
.gr-rank-cap {
  font-family: ui-monospace, 'SFMono-Regular', 'Courier New', monospace;
  font-size: var(--fs-sm);
  color: var(--color-text-faint);
  white-space: pre-line;
  margin: 30px 0 12px;
}
/* same 720px cap as .gr-vrows above -- the ranking table (NLH/PLO/7 Card Stud) has the same width:100% stretch problem. */
.gr-table-scroll { overflow-x: auto; margin: 0 0 24px; max-width: 900px; }
.gr-rank-table { border-collapse: collapse; width: 100%; min-width: 20rem; }
.gr-rank-table th, .gr-rank-table td {
  text-align: left;
  padding: 10px 14px;
  border-bottom: 1px solid var(--color-border);
}
.gr-rank-table th:first-child, .gr-rank-table td:first-child { padding-left: 0; }
.gr-rank-table th[scope="row"] { font-weight: 500; font-size: var(--fs-sm); color: var(--color-text); white-space: nowrap; }
.gr-rank-table tbody tr:last-child th, .gr-rank-table tbody tr:last-child td { border-bottom: none; }
.gr-rank-table td:last-child { text-align: right; }
.gr-rank-table td .gr-hand { margin: 0; justify-content: flex-end; }

@media (max-width: 640px) {
  .gr-switch { top: 0; }
  .gr-step { grid-template-columns: 1fr; gap: 2px; }
  .gr-vrow { grid-template-columns: 1fr; }
  .gr-amount { justify-self: start; }
  .gr-prow { grid-template-columns: 1fr auto; }
  .gr-kind { grid-column: 1 / -1; }
  .gr-card { width: 34px; height: 48px; }
  .gr-card-rank { height: 15px; }
  .gr-card-suit { height: 11px; }
  .gr-hand-group { padding: 4px; gap: 4px; }
  .gr-hand-arrow { font-size: 1.05rem; }
}
