/* ============================================================================
   Tarneeb shared components — built to the design handoff.

   One implementation of each thing the product needs everywhere: navigation,
   header, buttons, fields, tags, avatars, notifications, empty/error/loading,
   confirm, and the bottom sheet. Screens compose these; they do not restyle
   them locally. Depends on design-tokens.css.
   ========================================================================= */

/* ---------------------------------------------------------------- base ---- */
/* Border-box everywhere. Without this, any control that is width:100% AND has
   horizontal padding (every input and every block button) overflows its
   container by exactly the padding — which is what happened on the first
   device pass: the fields ran off the right edge of the phone. */
*, *::before, *::after { box-sizing: border-box; }

/* The hidden attribute must win. Any element given display:flex/grid by a rule
   silently ignores [hidden] — which is how an error state shipped with its
   pager still showing on the first device pass. */
[hidden] { display: none !important; }

.t-page {
  min-height: 100dvh;
  background: var(--bg);
  color: var(--ink);
  font-family: var(--font-ui);
  display: flex;
  flex-direction: column;
}

/* Content scrolls between a fixed header and the tab bar. */
.t-scroll {
  flex: 1 1 auto;
  overflow-y: auto;
  -webkit-overflow-scrolling: touch;
  padding: var(--sp-16) var(--sp-16) var(--sp-26);
  max-width: 560px;
  width: 100%;
  margin: 0 auto;
}

/* --------------------------------------------------------------- header --- */
/* 52px, flush-left title, 2px section rule, one optional right slot. Replaces
   the global top bar, which is gone: each screen owns its own header. */
.t-header {
  flex: none;
  height: var(--header-h);
  min-height: var(--header-h);
  display: flex;
  align-items: center;
  gap: var(--sp-10);
  padding: 0 var(--sp-16);
  background: var(--surf);
  border-bottom: var(--header-rule);
}
.t-header h1,
.t-header .t-title {
  font: 800 15px/1 var(--font-ui);
  letter-spacing: 0;
  margin: 0;
  color: var(--ink);
}
.t-header .t-header-right { margin-inline-start: auto; display: flex; align-items: center; gap: var(--sp-6); }
.t-header .t-back {
  display: flex; align-items: center; justify-content: center;
  width: 32px; height: var(--tap); margin-inline-start: -8px;
  color: var(--ink2); background: none; border: 0; cursor: pointer;
}
.t-header .t-back:hover { color: var(--ink); }

/* ------------------------------------------------------------- tab bar ---- */
/* Present on every screen except the table. A game is a context you enter and
   leave deliberately — tabs there invite a mis-tap out of a live match. */
.t-tabbar {
  flex: none;
  height: var(--tabbar-h);
  min-height: var(--tabbar-h);
  display: grid;
  /* minmax(0, 1fr), not 1fr: a bare 1fr track refuses to shrink below its
     content's min-width, and .t-tab used to carry min-width:78px. Five of them
     pinned the bar to exactly 390px, so on a 360px Android — or a 344px Fold
     cover screen — the fifth tab (YOU) hung 30-46px off the right edge with its
     icon sliced in half. Measured with a real 344px viewport; a cropped desktop
     screenshot cannot show this. */
  grid-template-columns: repeat(5, minmax(0, 1fr));
  background: var(--surf);
  border-top: 1px solid var(--line2);
  padding-bottom: env(safe-area-inset-bottom, 0);
}
.t-tab {
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  gap: 4px;
  /* No width floor — the grid divides the bar evenly instead. At the narrowest
     supported width (344px) that is 68px per tab, still comfortably above the
     44px minimum tap target, which is the constraint that actually matters. */
  min-width: 0;
  min-height: var(--tap);
  overflow: hidden;
  background: none; border: 0; cursor: pointer;
  color: var(--ink3);
  font: 700 9px/1 var(--font-ui);
  letter-spacing: .1em;
  text-transform: uppercase;
  text-decoration: none;
  white-space: nowrap;
  box-shadow: inset 0 2px 0 transparent;
}
/* Standalone pages own their layout, so the bar is fixed there and the page
   reserves room for it rather than being restructured into a flex column. */
.t-tabbar--fixed { position: fixed; inset-inline: 0; bottom: 0; z-index: 800; }
body.has-tabbar { padding-bottom: calc(var(--tabbar-h) + env(safe-area-inset-bottom, 0px)); }
.t-tab svg { width: 19px; height: 19px; stroke: currentColor; stroke-width: 2; fill: none; }
.t-tab:hover { color: var(--ink2); }
/* Active is a 2px gold inset rule on the top edge, not a filled pill — a pill
   fights the table's own chrome. */
.t-tab.is-active { color: var(--gold); box-shadow: inset 0 2px 0 var(--gold); }

/* -------------------------------------------------------------- buttons --- */
/* Five variants. Labels are flush-left in all of them so a full-width and a
   narrow button read as the same object. */
.t-btn {
  display: flex; align-items: center;
  min-height: 48px;
  padding: 0 var(--sp-14);
  gap: var(--sp-9);
  border: 0;
  border-radius: var(--radius);
  font: 800 15px/1 var(--font-ui);
  color: var(--ink);
  background: var(--surf3);
  cursor: pointer;
  white-space: nowrap;
  text-decoration: none;
  transition: filter 120ms ease, background-color 120ms ease;
}
.t-btn:hover { filter: brightness(1.08); }
.t-btn:active { filter: brightness(.95); }
.t-btn--block { width: 100%; }
.t-btn--center { justify-content: center; }

.t-btn--primary { background: var(--gold); color: var(--on-gold); font-weight: 800; }
.t-btn--secondary { background: var(--surf3); border: 1px solid var(--line4); color: var(--ink); }
.t-btn--ghost { background: none; border: 1px solid var(--line2); color: var(--ink2); }
.t-btn--destructive { background: var(--surf3); border: 1px solid #4a221d; color: #d4675c; }
.t-btn[disabled],
.t-btn--disabled {
  background: var(--surf2); color: var(--ink3); border-color: var(--line2);
  opacity: .45; cursor: default; filter: none;
}
.t-btn--sm { min-height: var(--tap); font-size: 13px; padding: 0 13px; }

/* --------------------------------------------------------------- fields --- */
.t-field { display: block; margin-bottom: var(--sp-14); }
.t-label {
  display: block;
  font: 700 10px/1 var(--font-ui);
  letter-spacing: .12em;
  text-transform: uppercase;
  color: var(--ink3);
  margin-bottom: var(--sp-6);
}
.t-input {
  width: 100%;
  min-height: 48px;
  padding: 0 var(--sp-12);
  background: var(--surf2);
  border: 1px solid var(--line3);
  border-radius: var(--radius);
  color: var(--ink);
  font: 400 13px/1.5 var(--font-ui);
  outline: none;
}
.t-input::placeholder { color: var(--ink4); }
.t-input:focus { border: 2px solid var(--gold); padding: 0 calc(var(--sp-12) - 1px); }
.t-input[aria-invalid="true"] { border-color: var(--red); }
.t-input-wrap { position: relative; }
/* Show/hide fills the field height so the target is the full side of the box. */
.t-reveal {
  position: absolute; inset-inline-end: 0; top: 0; bottom: 0;
  padding: 0 var(--sp-12);
  background: none; border: 0; cursor: pointer;
  color: var(--ink3);
  font: 700 10px/1 var(--font-ui); letter-spacing: .1em; text-transform: uppercase;
}
/* Errors sit under the field they belong to. */
.t-field-error {
  display: block;
  margin-top: var(--sp-6);
  font: 400 11px/1.5 var(--font-ui);
  color: var(--red);
}
.t-hint { display: block; margin-top: var(--sp-6); font: 400 11px/1.5 var(--font-ui); color: var(--ink3); }

/* Password strength — four segments. */
.t-strength { display: grid; grid-template-columns: repeat(4, 1fr); gap: 4px; margin-top: var(--sp-6); }
.t-strength i { height: 3px; background: var(--line2); display: block; }
.t-strength.s1 i:nth-child(-n+1),
.t-strength.s2 i:nth-child(-n+2),
.t-strength.s3 i:nth-child(-n+3),
.t-strength.s4 i:nth-child(-n+4) { background: var(--gold); }

/* Password rules, shown before typing rather than learned from an error. */
.t-rules { list-style: none; margin: var(--sp-6) 0 0; padding: 0; display: grid; gap: 3px; }
.t-rules li { font: 400 11px/1.5 var(--font-ui); color: var(--ink3); display: flex; gap: var(--sp-6); }
.t-rules li::before { content: "○"; color: var(--ink4); }
.t-rules li.ok { color: var(--green); }
.t-rules li.ok::before { content: "●"; color: var(--green); }

/* The whole row is the target for a checkbox. */
.t-check { display: flex; align-items: center; gap: var(--sp-10); min-height: var(--tap); cursor: pointer; }
.t-check input { width: 18px; height: 18px; accent-color: var(--gold); flex: none; }
.t-check span { font: 400 12px/1.5 var(--font-ui); color: var(--ink2); }

/* ----------------------------------------------------------- surfaces ----- */
.t-card {
  background: var(--surf2);
  border: 1px solid var(--line2);
  border-radius: var(--radius);
  padding: var(--sp-16);
}
.t-kicker {
  font: 700 9px/1 var(--font-ui);
  letter-spacing: .14em;
  text-transform: uppercase;
  color: var(--gold-dim);
  margin-bottom: var(--sp-9);
}
.t-display { font: 800 24px/1.1 var(--font-ui); letter-spacing: -.02em; margin: 0 0 var(--sp-6); }
.t-body { font: 400 13px/1.6 var(--font-ui); color: var(--ink2); margin: 0 0 var(--sp-12); }
.t-section-title {
  font: 700 9px/1 var(--font-ui); letter-spacing: .16em; text-transform: uppercase;
  color: var(--ink3); margin: var(--sp-20) 0 var(--sp-10);
}

/* ------------------------------------------------------------ tag/badge --- */
.t-tag {
  display: inline-flex; align-items: center;
  padding: 3px 6px;
  font: 800 9px/1 var(--font-ui); letter-spacing: .1em; text-transform: uppercase;
  border: 1px solid var(--line2); background: var(--surf3); color: var(--ink2);
}
.t-tag--gold { background: var(--goldwash); border-color: var(--line5); color: var(--gold-hi); }
.t-tag--win { background: rgba(125,154,106,.14); border-color: var(--green); color: var(--green); }
.t-tag--loss { background: rgba(184,51,42,.14); border-color: var(--red); color: var(--red); }
.t-tag--outline { background: none; }

/* --------------------------------------------------------------- avatar --- */
.t-avatar {
  position: relative; flex: none;
  width: 38px; height: 38px;
  background: var(--surf4);
  border: 1px solid var(--line4);
  display: flex; align-items: center; justify-content: center;
  font: 700 14px/1 var(--font-ui); color: var(--ink2);
  overflow: hidden;
}
.t-avatar img { width: 100%; height: 100%; object-fit: cover; display: block; }
.t-avatar--34 { width: 34px; height: 34px; font-size: 13px; }
.t-avatar--44 { width: 44px; height: 44px; font-size: 16px; }
.t-avatar--52 { width: 52px; height: 52px; font-size: 18px; }
.t-avatar--72 { width: 72px; height: 72px; font-size: 24px; }
/* Presence is never colour-only — the dot always pairs with a text state. */
.t-presence {
  position: absolute; inset-inline-end: -2px; bottom: -2px;
  width: 11px; height: 11px; border-radius: 50%;
  border: 2px solid var(--surf2);
  background: var(--line6);
}
.t-presence--online { background: var(--green); }
.t-presence--ingame { background: var(--gold); }

/* Visible to a screen reader, not to the eye. For labels whose wording is
   already carried by a surrounding heading or disclosure row — the binding is
   what assistive tech needs, the second copy of the words is not.
   `clip-path` rather than `display:none`, because a hidden element is not
   announced at all. */
.t-visually-hidden {
  position: absolute;
  width: 1px; height: 1px;
  margin: -1px; padding: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

/* --------------------------------------------------- notification (one) --- */
/* Replaces five separate systems. One anchor: bottom, above the tab bar, so it
   can never collide with the score row. Intent is a 3px left border only. */
#t-toasts {
  position: fixed;
  inset-inline: 0;
  bottom: calc(var(--tabbar-h) + env(safe-area-inset-bottom, 0px) + var(--sp-10));
  z-index: 9000;
  display: flex; flex-direction: column; align-items: center; gap: var(--sp-9);
  pointer-events: none;
  padding: 0 var(--sp-12);
}
.t-toast {
  pointer-events: auto;
  position: relative;
  width: 100%; max-width: 520px;
  background: var(--surf3);
  border: 1px solid var(--line2);
  border-inline-start: var(--toast-border) solid var(--toast-neutral);
  padding: var(--sp-12) var(--sp-14);
  color: var(--ink);
  font: 400 13px/1.5 var(--font-ui);
  display: flex; align-items: center; gap: var(--sp-12);
  opacity: 0; transform: translateY(6px);
  transition: opacity 180ms ease, transform 180ms ease;
}
.t-toast.in { opacity: 1; transform: none; }
.t-toast--info { border-inline-start-color: var(--toast-info); }
.t-toast--success { border-inline-start-color: var(--toast-success); }
.t-toast--error { border-inline-start-color: var(--toast-error); }
/* Four intents, per the handoff. `warn` was the missing one — the rogue toast
   system in app.js had it, this component did not, so consolidating onto the
   component would have silently downgraded every warning to neutral. */
.t-toast--warn { border-inline-start-color: var(--gold-dim, #8b7843); }

/* At the table the lane moves to the felt's bottom edge: above the hand band,
   below the trick. The app bar is hidden in-game (body.in-game) and the tab bar
   is absent, so the default offset would leave it floating over the cards.
   This rule used to live on the old `.toast-host`; it moved here with the
   consolidation, because deleting the rogue host without porting it would have
   dropped the toast straight onto the hand. */
body.in-game #t-toasts {
  bottom: calc(var(--card-w, 84px) * 1.4 + 74px);
  padding: 0 8px;
}
body.in-game .t-toast {
  max-width: 88%;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.t-toast-msg { flex: 1 1 auto; }
.t-toast-action {
  flex: none; background: none; border: 0; cursor: pointer;
  color: var(--gold); font: 700 11px/1 var(--font-ui);
  letter-spacing: .1em; text-transform: uppercase;
  min-height: var(--tap); display: flex; align-items: center; padding: 0 var(--sp-6);
}
/* A 2px underline drains as the dismiss timer, so nothing vanishes unannounced. */
.t-toast-timer {
  position: absolute; left: 0; bottom: 0; height: 2px;
  background: var(--line6); width: 100%;
  transform-origin: left center;
  animation: tToastDrain linear forwards;
}
@keyframes tToastDrain { from { transform: scaleX(1); } to { transform: scaleX(0); } }
@media (prefers-reduced-motion: reduce) {
  .t-toast { transition: none; }
  .t-toast-timer { animation: none; }
}

/* -------------------------------------------- empty / error / loading ----- */
/* Three distinct states. An outage used to render as an empty leaderboard and a
   failed friends list rendered as "sign in"; they are now different objects. */
.t-empty, .t-error {
  text-align: center;
  padding: var(--sp-26) var(--sp-16);
  border: 1px dashed var(--line3);
}
.t-error { border: 1px solid #4a221d; border-style: solid; }
.t-empty h3, .t-error h3 { font: 800 15px/1.2 var(--font-ui); margin: 0 0 var(--sp-6); color: var(--ink); }
.t-empty p, .t-error p { font: 400 12px/1.5 var(--font-ui); color: var(--ink3); margin: 0 0 var(--sp-14); }
.t-empty .t-btn, .t-error .t-btn { display: inline-flex; }

.t-skeleton { display: grid; gap: var(--sp-9); }
.t-skeleton i {
  display: block; height: 52px; background: var(--surf2);
  animation: tPulse 1400ms ease-in-out infinite;
}
.t-skeleton i:nth-child(2) { animation-delay: 160ms; }
.t-skeleton i:nth-child(3) { animation-delay: 320ms; }
.t-skeleton i:nth-child(4) { animation-delay: 480ms; }
@keyframes tPulse { 0%,100% { opacity: .5; } 50% { opacity: 1; } }
@media (prefers-reduced-motion: reduce) { .t-skeleton i { animation: none; } }

/* ---------------------------------------------------- sheet + confirm ----- */
.t-scrim {
  position: fixed; inset: 0; z-index: 9500;
  background: rgba(0,0,0,.62);
  display: flex; align-items: flex-end; justify-content: center;
}
.t-scrim[hidden] { display: none; }
/* Every panel and sheet carries the same 2px gold top edge. */
.t-sheet {
  width: 100%; max-width: 520px;
  background: var(--surf);
  border-top: var(--rule-active);
  padding: var(--sp-18) var(--sp-16) calc(var(--sp-18) + env(safe-area-inset-bottom, 0px));
  animation: tSheetUp 260ms var(--ease-overshoot) both;
}
@keyframes tSheetUp { from { transform: translateY(14px); opacity: 0; } to { transform: none; opacity: 1; } }
@media (prefers-reduced-motion: reduce) { .t-sheet { animation-duration: 80ms; } }
.t-sheet h2 { font: 800 18px/1.2 var(--font-ui); margin: 0 0 var(--sp-9); }
/* The consequence goes in the body — a confirm that only asks "are you sure?"
   teaches nothing. */
.t-sheet p { font: 400 13px/1.6 var(--font-ui); color: var(--ink2); margin: 0 0 var(--sp-18); }
.t-sheet-actions { display: grid; gap: var(--sp-9); }

/* ----------------------------------------------------------- utilities ---- */
.t-row { display: flex; align-items: center; gap: var(--sp-12); }
.t-row-between { display: flex; align-items: center; justify-content: space-between; gap: var(--sp-12); }
.t-list { list-style: none; margin: 0; padding: 0; display: grid; gap: var(--sp-9); }
.t-list-row {
  display: flex; align-items: center; gap: var(--sp-12);
  padding: var(--sp-10) var(--sp-12);
  background: var(--surf2);
  border: 1px solid var(--line2);
  min-height: 52px;
  text-decoration: none; color: inherit;
}
a.t-list-row:hover, button.t-list-row:hover { border-color: var(--line4); }
.t-list-row.is-you { background: var(--goldwash); border-color: var(--line5); }
.t-name { font: 700 13px/1.3 var(--font-ui); color: var(--ink); }
.t-meta { font: 400 11px/1.4 var(--font-ui); color: var(--ink3); }
.t-sr {
  position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
  overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0;
}

/* A suit glyph is always a white chip with a card border, in every context, so
   spades read black and hearts read red regardless of theme. */
.t-suit-chip {
  display: inline-flex; align-items: center; justify-content: center;
  min-width: 22px; height: 22px; padding: 0 4px;
  background: var(--card); border: 1px solid var(--card-line);
  color: var(--card-ink);
  font: 800 13px/1 var(--font-ui);
}
.t-suit-chip.red { color: var(--card-red); }
