/* ===========================================================================
   HuuTichSop Live — design tokens and components
   ---------------------------------------------------------------------------
   One stylesheet shared by every control surface (admin, deck, pad, likeboard
   admin) so they read as one product. The palette is lifted from the logo:
   cyan → blue → violet → pink. Those colours are loud, so they are spent only
   on what the eye should land on — the active nav item, the primary button, a
   live badge. Everything else sits on a near-white lavender ground with plain
   dark slate text, which is what keeps a dense control panel readable for a
   whole stream.
   =========================================================================== */

:root {
  /* Brand ramp, straight off the logo */
  --brand-cyan: #29c5f6;
  --brand-blue: #3b82f6;
  --brand-violet: #7c5cf6;
  --brand-pink: #f0479b;

  --brand-gradient: linear-gradient(135deg, var(--brand-cyan), var(--brand-blue) 42%, var(--brand-violet) 74%, var(--brand-pink));
  --brand-gradient-soft: linear-gradient(135deg, rgba(41, 197, 246, 0.12), rgba(124, 92, 246, 0.12) 55%, rgba(240, 71, 155, 0.12));

  /* TikTok's own red, the colour of the one button it wants pressed */
  --tt-red: #fe2c55;
  --tt-red-hover: #ff4568;
  --tt-cyan: #25f4ee;

  /* Surfaces. Near-black page, one step up for panels, one more for anything
     sunken inside a panel — the same three-step ladder TikTok's web app uses.
     Not pure #000 for panels: a flat black card on a black page has no edge,
     and the whole layout collapses into one dark rectangle. */
  --surface-app: #0f0f12;
  --surface-card: #1a1a1f;
  --surface-sunken: #242429;
  --surface-nav: #000000;

  /* Ink */
  --ink-strong: #ffffff;
  --ink: #e3e3e8;
  --ink-muted: #8b8b96;

  --line: #2b2b32;
  --line-strong: #3d3d46;

  /* Status. On dark, a pale tint reads as a grey smudge, so the "soft" pairs
     are translucent washes of the colour itself instead. */
  --ok: #2ad17f;
  --ok-soft: rgba(42, 209, 127, 0.14);
  --warn: #f0a93c;
  --warn-soft: rgba(240, 169, 60, 0.15);
  --danger: #ff4a5f;
  --danger-soft: rgba(255, 74, 95, 0.15);

  --radius: 14px;
  --radius-sm: 10px;

  /* Shadows stay tinted rather than grey — a neutral shadow on a tinted ground
     reads as dirt. */
  /* Shadows barely register on a dark ground, so depth comes mostly from the
     surface ladder above; these only soften edges. */
  --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.4);
  --shadow-md: 0 6px 18px rgba(0, 0, 0, 0.5);
  --shadow-lift: 0 12px 32px rgba(0, 0, 0, 0.6);

  /* One duration and one curve everywhere, so nothing feels out of step */
  --ease: cubic-bezier(0.22, 0.61, 0.36, 1);
  --fast: 120ms;
  --normal: 200ms;
}

/* --------------------------------------------------------------------------
   Base
   -------------------------------------------------------------------------- */

body {
  background: var(--surface-app);
  color: var(--ink);
  -webkit-font-smoothing: antialiased;
}

::selection {
  background: rgba(124, 92, 246, 0.18);
}

/* A single focus treatment for the whole app. focus-visible only, so a mouse
   click never leaves a ring behind but keyboard users always see where they
   are. */
:where(button, a, input, select, textarea, [tabindex]):focus-visible {
  outline: 2px solid var(--brand-violet);
  outline-offset: 2px;
  border-radius: var(--radius-sm);
}

/* Scrollbars, toned down so long lists do not draw a hard grey stripe */
* {
  scrollbar-width: thin;
  scrollbar-color: var(--line-strong) transparent;
}
*::-webkit-scrollbar {
  width: 10px;
  height: 10px;
}
*::-webkit-scrollbar-thumb {
  background: var(--line-strong);
  border-radius: 999px;
  border: 3px solid transparent;
  background-clip: content-box;
}
*::-webkit-scrollbar-thumb:hover {
  background: var(--ink-muted);
  background-clip: content-box;
}

/* --------------------------------------------------------------------------
   Brand marks
   -------------------------------------------------------------------------- */

.brand-text {
  background: var(--brand-gradient);
  -webkit-background-clip: text;
  background-clip: text;
  color: transparent;
  font-weight: 800;
  letter-spacing: -0.02em;
}

/* --------------------------------------------------------------------------
   Cards
   -------------------------------------------------------------------------- */

.card {
  background: var(--surface-card);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  box-shadow: var(--shadow-sm);
}

.card-pad {
  padding: 20px;
}

.card-title {
  font-size: 14px;
  font-weight: 650;
  color: var(--ink-strong);
  letter-spacing: -0.01em;
}

.card-hint {
  font-size: 12px;
  color: var(--ink-muted);
  line-height: 1.55;
}

/* A card that is the page's headline action gets the brand wash instead of an
   outline, so there is exactly one obvious starting point per screen. */
.card-accent {
  background: var(--brand-gradient-soft);
  border-color: transparent;
}

/* --------------------------------------------------------------------------
   Buttons
   -------------------------------------------------------------------------- */

.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 6px;
  height: 36px;
  padding: 0 14px;
  border-radius: var(--radius-sm);
  border: 1px solid transparent;
  font-size: 13px;
  font-weight: 600;
  line-height: 1;
  cursor: pointer;
  white-space: nowrap;
  transition: transform var(--fast) var(--ease), box-shadow var(--normal) var(--ease),
    background-color var(--normal) var(--ease), border-color var(--normal) var(--ease),
    color var(--normal) var(--ease), filter var(--normal) var(--ease);
}

/* Lift on hover, sink on press. Two frames of movement is all a click needs to
   feel physical; anything more starts to feel slow after the hundredth press. */
.btn:hover:not(:disabled) {
  transform: translateY(-1px);
}
.btn:active:not(:disabled) {
  transform: translateY(0) scale(0.975);
  transition-duration: 60ms;
}
.btn:disabled {
  opacity: 0.5;
  cursor: not-allowed;
}

.btn-primary {
  background: var(--brand-gradient);
  background-size: 160% 160%;
  color: #fff;
  box-shadow: 0 2px 10px rgba(90, 80, 220, 0.28);
}
.btn-primary:hover:not(:disabled) {
  filter: saturate(1.08);
  box-shadow: var(--shadow-lift);
}

.btn-ghost {
  background: var(--surface-card);
  border-color: var(--line-strong);
  color: var(--ink);
}
.btn-ghost:hover:not(:disabled) {
  border-color: var(--brand-violet);
  color: var(--brand-violet);
  box-shadow: var(--shadow-md);
}

.btn-quiet {
  background: transparent;
  color: var(--ink-muted);
}
.btn-quiet:hover:not(:disabled) {
  background: var(--surface-sunken);
  color: var(--ink);
}

.btn-danger {
  background: var(--danger-soft);
  color: var(--danger);
}
.btn-danger:hover:not(:disabled) {
  background: var(--danger);
  color: #fff;
}

.btn-sm {
  height: 30px;
  padding: 0 10px;
  font-size: 12px;
  border-radius: 8px;
}

.btn-icon {
  width: 34px;
  padding: 0;
}

/* --------------------------------------------------------------------------
   Form controls
   -------------------------------------------------------------------------- */

.field {
  display: flex;
  flex-direction: column;
  gap: 5px;
}

.field-label {
  font-size: 11px;
  font-weight: 600;
  color: var(--ink-muted);
  text-transform: uppercase;
  letter-spacing: 0.04em;
}

.input,
.select {
  height: 36px;
  padding: 0 11px;
  border: 1px solid var(--line-strong);
  border-radius: var(--radius-sm);
  background: var(--surface-card);
  color: var(--ink-strong);
  font-size: 13px;
  transition: border-color var(--normal) var(--ease), box-shadow var(--normal) var(--ease);
}
.input::placeholder {
  color: var(--ink-muted);
  opacity: 0.7;
}
.input:hover,
.select:hover {
  border-color: var(--ink-muted);
}
.input:focus,
.select:focus {
  outline: none;
  border-color: var(--brand-violet);
  box-shadow: 0 0 0 3px rgba(124, 92, 246, 0.14);
}

.select {
  appearance: none;
  padding-right: 30px;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpath d='M2.5 4.5L6 8l3.5-3.5' stroke='%237d83a8' stroke-width='1.6' fill='none' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: right 10px center;
}

.check {
  display: flex;
  align-items: flex-start;
  gap: 9px;
  cursor: pointer;
  font-size: 12px;
  color: var(--ink);
  line-height: 1.5;
}
.check input[type="checkbox"] {
  margin-top: 2px;
  width: 16px;
  height: 16px;
  accent-color: var(--brand-violet);
  cursor: pointer;
  flex-shrink: 0;
}

/* --------------------------------------------------------------------------
   Badges
   -------------------------------------------------------------------------- */

.badge {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  height: 28px;
  padding: 0 11px;
  border-radius: 999px;
  font-size: 12px;
  font-weight: 600;
  transition: background-color var(--normal) var(--ease), color var(--normal) var(--ease);
}
.badge-neutral {
  background: var(--surface-sunken);
  color: var(--ink-muted);
}
.badge-ok {
  background: var(--ok-soft);
  color: var(--ok);
}
.badge-warn {
  background: var(--warn-soft);
  color: var(--warn);
}
.badge-danger {
  background: var(--danger-soft);
  color: var(--danger);
}

/* A dot that breathes only while something really is live. Slow and low
   contrast on purpose — a fast blink in the corner of a control panel is
   exhausting to sit next to. */
.badge-dot {
  width: 7px;
  height: 7px;
  border-radius: 999px;
  background: currentColor;
}
.badge-ok .badge-dot {
  animation: pulse-dot 2.4s var(--ease) infinite;
}

@keyframes pulse-dot {
  0%, 100% { opacity: 1; transform: scale(1); }
  50% { opacity: 0.45; transform: scale(0.82); }
}

/* --------------------------------------------------------------------------
   Tables
   -------------------------------------------------------------------------- */

.table {
  width: 100%;
  border-collapse: collapse;
  font-size: 13px;
}
.table th {
  text-align: left;
  padding: 8px 10px;
  font-size: 10px;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--ink-muted);
  border-bottom: 1px solid var(--line);
}
.table td {
  padding: 11px 10px;
  border-bottom: 1px solid var(--line);
  color: var(--ink);
}
.table tbody tr {
  transition: background-color var(--fast) var(--ease);
}
.table tbody tr:hover {
  background: var(--surface-sunken);
}
.table tbody tr:last-child td {
  border-bottom: none;
}

/* --------------------------------------------------------------------------
   Entry motion
   -------------------------------------------------------------------------- */

@keyframes rise {
  from { opacity: 0; transform: translateY(8px); }
  to { opacity: 1; transform: none; }
}

.rise {
  animation: rise 380ms var(--ease) both;
}
.rise-1 { animation-delay: 40ms; }
.rise-2 { animation-delay: 80ms; }
.rise-3 { animation-delay: 120ms; }
.rise-4 { animation-delay: 160ms; }

@keyframes fade-scale {
  from { opacity: 0; transform: scale(0.97); }
  to { opacity: 1; transform: none; }
}

.pop {
  animation: fade-scale 180ms var(--ease) both;
}

/* Anyone who has asked their OS for less motion gets none of it. The layout
   never depends on these animations, so switching them off costs nothing. */
@media (prefers-reduced-motion: reduce) {
  *,
  *::before,
  *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
  }
  .btn:hover:not(:disabled),
  .btn:active:not(:disabled) {
    transform: none;
  }
}

/* --------------------------------------------------------------------------
   Page-specific pieces that still want the shared motion language
   -------------------------------------------------------------------------- */

.video-card {
  background: var(--surface-card);
  overflow: hidden;
  transition: transform var(--normal) var(--ease), box-shadow var(--normal) var(--ease),
    border-color var(--normal) var(--ease);
}
.video-card:hover {
  transform: translateY(-2px);
  border-color: var(--brand-violet);
  box-shadow: var(--shadow-md);
}

.gift-tile {
  background: var(--surface-card);
  border-color: var(--line);
  cursor: pointer;
  transition: transform var(--fast) var(--ease), border-color var(--normal) var(--ease),
    background-color var(--normal) var(--ease), box-shadow var(--normal) var(--ease);
}
.gift-tile:hover {
  transform: translateY(-2px);
  border-color: var(--brand-violet);
  background: #faf8ff;
  box-shadow: var(--shadow-md);
}
.gift-tile:active {
  transform: translateY(0) scale(0.97);
  transition-duration: 60ms;
}
.gift-tile img {
  transition: transform var(--normal) var(--ease);
}
.gift-tile:hover img {
  transform: scale(1.12);
}

/* --------------------------------------------------------------------------
   Stream Deck
   -------------------------------------------------------------------------- */

.deck-card {
  background: var(--surface-card);
  border: 1px solid var(--line);
  box-shadow: var(--shadow-sm);
  transition: transform var(--normal) var(--ease), box-shadow var(--normal) var(--ease),
    border-color var(--normal) var(--ease);
}
.deck-card:hover {
  transform: translateY(-2px);
  box-shadow: var(--shadow-md);
}
/* The playing ring is the one thing on this page that must be readable at a
   glance from across the room, so it gets the full-strength green plus a halo
   rather than a colour swap alone. */
.deck-card.is-playing {
  border-color: var(--ok);
  box-shadow: 0 0 0 3px rgba(14, 163, 113, 0.22), var(--shadow-md);
}


/* Ô vừa được bấm, server chưa xác nhận. Nhịp đập nhẹ cộng một vệt sáng quét qua
   để người bấm biết máy đã nhận lệnh.

   Vì sao cần: trước đây bấm xong nút đứng im cho tới lúc video thật sự chạy,
   nên người dùng tưởng bấm hụt và bấm thêm lần nữa — lệnh thứ hai bị hiểu là
   "dừng", hoặc tệ hơn là xếp thêm một lượt phát trùng. */
.deck-card.is-pending {
  position: relative;
  border-color: var(--tt-red);
  animation: deck-pending-pulse 900ms ease-in-out infinite;
}
.deck-card.is-pending::after {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: inherit;
  pointer-events: none;
  background: linear-gradient(
    100deg,
    transparent 30%,
    rgba(255, 255, 255, 0.18) 50%,
    transparent 70%
  );
  background-size: 250% 100%;
  animation: deck-shimmer 1100ms linear infinite;
}

@keyframes deck-pending-pulse {
  0%,
  100% {
    box-shadow: 0 0 0 0 rgba(254, 44, 85, 0.45), var(--shadow-md);
  }
  50% {
    box-shadow: 0 0 0 7px rgba(254, 44, 85, 0), var(--shadow-md);
  }
}

@keyframes deck-shimmer {
  from {
    background-position: 150% 0;
  }
  to {
    background-position: -50% 0;
  }
}

/* Ô trên bảng pad (cửa sổ Stream Deck rút gọn) dùng cấu trúc khác, nhưng cùng
   ngôn ngữ hình ảnh. */
.tile.is-pending {
  position: relative;
  animation: deck-pending-pulse 900ms ease-in-out infinite;
}
.tile.is-pending::after {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: inherit;
  pointer-events: none;
  background: linear-gradient(
    100deg,
    transparent 30%,
    rgba(255, 255, 255, 0.18) 50%,
    transparent 70%
  );
  background-size: 250% 100%;
  animation: deck-shimmer 1100ms linear infinite;
}

/* Ai bật giảm chuyển động thì bỏ hẳn hiệu ứng, chỉ giữ viền đổi màu. */
@media (prefers-reduced-motion: reduce) {
  .deck-card.is-pending,
  .tile.is-pending {
    animation: none;
  }
  .deck-card.is-pending::after,
  .tile.is-pending::after {
    display: none;
  }
}

.deck-face {
  cursor: pointer;
  transition: transform var(--fast) var(--ease);
}
.deck-face:active {
  transform: scale(0.97);
  transition-duration: 60ms;
}

.deck-add {
  border: 2px dashed var(--line-strong);
  background: transparent;
  color: var(--ink-muted);
  cursor: pointer;
  transition: color var(--normal) var(--ease), border-color var(--normal) var(--ease),
    background-color var(--normal) var(--ease), transform var(--fast) var(--ease);
}
.deck-add:hover {
  border-color: var(--brand-violet);
  color: var(--brand-violet);
  background: rgba(124, 92, 246, 0.05);
}
.deck-add:active {
  transform: scale(0.97);
}

.deck-mini-select {
  height: 22px;
  padding: 0 4px;
  font-size: 10px;
  border: 1px solid var(--line);
  border-radius: 6px;
  background: var(--surface-card);
  color: var(--ink);
  transition: border-color var(--normal) var(--ease);
}
.deck-mini-select:hover {
  border-color: var(--line-strong);
}
.deck-mini-select:focus {
  outline: none;
  border-color: var(--brand-violet);
}

.deck-mini-danger {
  font-size: 10px;
  font-weight: 600;
  color: var(--danger);
  padding: 0 4px;
  border-radius: 5px;
  cursor: pointer;
  background: transparent;
  transition: background-color var(--normal) var(--ease);
}
.deck-mini-danger:hover {
  background: var(--danger-soft);
}

/* --------------------------------------------------------------------------
   Likeboard admin: colour swatches and segmented toggles
   -------------------------------------------------------------------------- */

.swatch-btn {
  opacity: 0.5;
  cursor: pointer;
  transition: opacity var(--normal) var(--ease), transform var(--fast) var(--ease),
    box-shadow var(--normal) var(--ease);
}
.swatch-btn:hover {
  opacity: 0.85;
  transform: translateY(-1px);
}
.swatch-btn:active {
  transform: scale(0.97);
}
.swatch-btn.is-active {
  opacity: 1;
  box-shadow: 0 0 0 2px var(--surface-card), 0 0 0 4px var(--brand-violet);
}

/* A two-button segmented control: the pair reads as one control, so only the
   selected half is filled and the divider belongs to the group, not a button. */
.seg-btn {
  background: var(--surface-card);
  color: var(--ink-muted);
  cursor: pointer;
  font-weight: 600;
  transition: background-color var(--normal) var(--ease), color var(--normal) var(--ease);
}
.seg-btn:hover:not(.is-active) {
  background: var(--surface-sunken);
  color: var(--ink);
}
.seg-btn.is-active {
  background: var(--brand-violet);
  color: #fff;
}
.seg-divider {
  border-left: 1px solid var(--line-strong);
}

/* ===========================================================================
   TikTok LIVE vocabulary
   ---------------------------------------------------------------------------
   Borrowed from the app the user stares at all day, so live data here reads
   the same way it does on their phone: near-black stage, translucent pill
   chips, a hairline-bordered ranking panel, medals for the top three and a
   gold bar for the row that matters. The light chrome around it stays as it
   is — controls want calm, live data wants the familiar look.
   =========================================================================== */

:root {
  /* Neutral blacks, matching the page ground. A blue-black panel on a neutral
     black page reads as a colour cast rather than as depth. */
  --tt-stage: #08080a;
  --tt-panel: rgba(28, 28, 33, 0.9);
  --tt-hairline: rgba(255, 255, 255, 0.16);
  --tt-chip: rgba(255, 255, 255, 0.10);
  --tt-text: #ffffff;
  --tt-text-dim: #9a9aa4;
  --tt-pink: #ff3f87;
  --tt-pink-soft: #ff7ab4;
  --tt-gold-1: #c98a1b;
  --tt-gold-2: #f2c14e;
  --tt-gold-edge: #ffdf8a;
}

/* The dark stage a live panel sits on */
.tt-stage {
  background: var(--tt-stage);
  color: var(--tt-text);
}

/* The "Ranking list" box: dark translucent fill, one hairline of light, big
   radius. The hairline is what makes it float over video rather than sit on it. */
.tt-panel {
  background: var(--tt-panel);
  border: 1px solid var(--tt-hairline);
  border-radius: 16px;
  backdrop-filter: blur(8px);
  color: var(--tt-text);
}

/* Comment/gift chips: full-round pills, translucent, never a hard edge */
.tt-chip {
  display: flex;
  align-items: center;
  gap: 9px;
  padding: 7px 13px 7px 8px;
  border-radius: 999px;
  background: var(--tt-chip);
  color: var(--tt-text);
  font-size: 12px;
  line-height: 1.35;
  width: fit-content;
  max-width: 100%;
  transition: background-color var(--normal) var(--ease);
}
.tt-chip:hover {
  background: rgba(255, 255, 255, 0.16);
}
.tt-chip-dim {
  color: var(--tt-text-dim);
}

/* Circular avatar with the thin bright ring TikTok puts on every face */
.tt-avatar {
  width: 28px;
  height: 28px;
  border-radius: 999px;
  object-fit: cover;
  flex-shrink: 0;
  background: #2c2c33;
  box-shadow: 0 0 0 1.5px rgba(255, 255, 255, 0.28);
}

/* Ranking rows */
.tt-rank {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 7px 10px;
  border-radius: 10px;
  transition: background-color var(--normal) var(--ease), transform var(--fast) var(--ease);
}
.tt-rank:hover {
  background: rgba(255, 255, 255, 0.07);
}
.tt-rank + .tt-rank {
  margin-top: 2px;
}

/* The highlighted row — gold fill with a bright top edge, the treatment TikTok
   reserves for "this is the one you care about". */
.tt-rank.is-top {
  background: linear-gradient(180deg, var(--tt-gold-2), var(--tt-gold-1));
  border: 1px solid var(--tt-gold-edge);
  color: #22190a;
  font-weight: 700;
}
.tt-rank.is-top .tt-rank-name,
.tt-rank.is-top .tt-rank-score {
  color: #22190a;
}

.tt-rank-pos {
  width: 24px;
  flex-shrink: 0;
  text-align: center;
  font-size: 13px;
  font-weight: 700;
  color: var(--tt-text-dim);
}
.tt-rank-name {
  flex: 1;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font-size: 13px;
  color: var(--tt-text);
}
.tt-rank-score {
  flex-shrink: 0;
  font-size: 13px;
  font-weight: 700;
  color: var(--tt-pink-soft);
}

/* The magenta meter under the countdown card in the reference */
.tt-meter {
  height: 10px;
  border-radius: 999px;
  overflow: hidden;
  background: rgba(255, 255, 255, 0.12);
}
.tt-meter-fill {
  height: 100%;
  border-radius: 999px;
  background: linear-gradient(90deg, var(--tt-pink), var(--tt-pink-soft));
  box-shadow: 0 0 12px rgba(255, 63, 135, 0.55);
  transition: width 500ms var(--ease);
}

/* Event-feed chip internals */
.tt-log-icon {
  font-size: 14px;
  line-height: 1;
  flex-shrink: 0;
}
.tt-log-text {
  min-width: 0;
  font-weight: 600;
}
.tt-log-time {
  flex-shrink: 0;
  font-size: 10px;
  color: var(--tt-text-dim);
  font-variant-numeric: tabular-nums;
}

/* A chip for the thing playing right now: TikTok's own live-pink, not the
   app's green, because this row is describing what is on the stream. */
.tt-chip-live {
  background: rgba(255, 63, 135, 0.18);
  box-shadow: inset 0 0 0 1px rgba(255, 122, 180, 0.45);
}
.tt-live-tag {
  padding: 2px 9px;
  border-radius: 999px;
  background: var(--tt-pink);
  color: #fff;
  font-size: 10px;
  font-weight: 700;
  letter-spacing: 0.02em;
}

/* ===========================================================================
   TikTok LIVE accent — the orange
   ---------------------------------------------------------------------------
   In the reference, the one button the viewer is meant to press ("Tham gia")
   is a warm orange pill, and the row that matters is gold. That orange is the
   loudest thing on a near-black screen, so it is given to exactly one job
   here too: the primary action. The logo ramp stays in the nav rail and on the
   brand mark, which keeps HuuTichSop's identity out of the way of the
   call-to-action instead of competing with it.
   =========================================================================== */

:root {
  --tt-orange-1: #fbab4d;
  --tt-orange-2: #f0812e;
  --tt-orange-3: #e2662a;
  --tt-orange-ink: #6d3407;
  --tt-orange-gradient: linear-gradient(135deg, var(--tt-orange-1), var(--tt-orange-2) 62%, var(--tt-orange-3));
  --tt-orange-soft: #fff2e4;

  /* Focus and selection follow the accent so the whole app agrees */
  --accent: var(--tt-orange-2);
}

/* The primary button takes the TikTok CTA treatment: full-round pill, warm
   gradient, warm shadow. Rounded fully rather than 10px — that pill shape is
   most of what makes it read as "the TikTok button". */
.btn-primary {
  background: var(--tt-orange-gradient);
  color: #fff;
  border-radius: 999px;
  box-shadow: 0 2px 12px rgba(240, 129, 46, 0.38);
}
.btn-primary:hover:not(:disabled) {
  filter: saturate(1.06) brightness(1.03);
  box-shadow: 0 8px 22px rgba(240, 129, 46, 0.42);
}

.btn-sm.btn-primary {
  border-radius: 999px;
}

/* A ghost button that belongs to the orange family, for secondary actions
   sitting next to a primary one. */
.btn-cta-quiet {
  background: var(--tt-orange-soft);
  border-color: transparent;
  color: var(--tt-orange-3);
}
.btn-cta-quiet:hover:not(:disabled) {
  background: #ffe6cd;
  color: var(--tt-orange-ink);
}

:where(button, a, input, select, textarea, [tabindex]):focus-visible {
  outline-color: var(--accent);
}

::selection {
  background: rgba(240, 129, 46, 0.22);
}

.input:focus,
.select:focus {
  border-color: var(--accent);
  box-shadow: 0 0 0 3px rgba(240, 129, 46, 0.16);
}

.check input[type="checkbox"] {
  accent-color: var(--accent);
}

.btn-ghost:hover:not(:disabled) {
  border-color: var(--accent);
  color: var(--tt-orange-3);
}

.gift-tile:hover,
.video-card:hover,
.deck-add:hover {
  border-color: var(--accent);
}
.deck-add:hover {
  color: var(--tt-orange-3);
  background: rgba(240, 129, 46, 0.06);
}
.gift-tile:hover {
  background: var(--tt-orange-soft);
}

.seg-btn.is-active {
  background: var(--tt-orange-gradient);
}

.swatch-btn.is-active {
  box-shadow: 0 0 0 2px var(--surface-card), 0 0 0 4px var(--accent);
}

/* The "Tham gia" pill itself, for anywhere a hard call-to-action is wanted */
.tt-cta {
  display: inline-flex;
  align-items: center;
  gap: 7px;
  height: 34px;
  padding: 0 16px;
  border-radius: 999px;
  background: var(--tt-orange-gradient);
  color: #fff;
  font-size: 13px;
  font-weight: 700;
  border: none;
  cursor: pointer;
  box-shadow: 0 3px 14px rgba(240, 129, 46, 0.45);
  transition: transform var(--fast) var(--ease), box-shadow var(--normal) var(--ease),
    filter var(--normal) var(--ease);
}
.tt-cta:hover {
  transform: translateY(-1px);
  filter: brightness(1.04);
}
.tt-cta:active {
  transform: scale(0.97);
  transition-duration: 60ms;
}

/* The warm level chip TikTok puts before a username */
.tt-tag {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  padding: 2px 8px;
  border-radius: 999px;
  background: linear-gradient(135deg, #b4442c, #7e2f22);
  color: #ffd9a8;
  font-size: 10px;
  font-weight: 700;
  flex-shrink: 0;
}

/* ===========================================================================
   Dark surface corrections
   ---------------------------------------------------------------------------
   Rules written when the app had a white ground and which do not simply follow
   the tokens: light hover washes, the accent card, and the primary button —
   which becomes TikTok's red, because on black that is the colour the eye goes
   to first and this app has exactly one "do the thing" button per screen.
   =========================================================================== */

.btn-primary {
  background: var(--tt-red);
  color: #fff;
  border-radius: 999px;
  box-shadow: none;
}
.btn-primary:hover:not(:disabled) {
  background: var(--tt-red-hover);
  filter: none;
  box-shadow: 0 6px 18px rgba(254, 44, 85, 0.35);
}

.btn-ghost {
  background: var(--surface-sunken);
  border-color: var(--line-strong);
  color: var(--ink);
}
.btn-ghost:hover:not(:disabled) {
  background: #2e2e35;
  border-color: var(--line-strong);
  color: #fff;
  box-shadow: none;
}

.btn-quiet:hover:not(:disabled) {
  background: rgba(255, 255, 255, 0.08);
  color: #fff;
}

.btn-danger {
  background: var(--danger-soft);
  color: var(--danger);
}
.btn-danger:hover:not(:disabled) {
  background: var(--danger);
  color: #fff;
}

.card-accent {
  background: var(--surface-card);
  border: 1px solid var(--line);
  position: relative;
  overflow: hidden;
}
/* A thin brand ribbon along the top edge instead of a wash — a tinted panel on
   a dark ground just looks like a different grey. */
.card-accent::before {
  content: "";
  position: absolute;
  inset: 0 0 auto 0;
  height: 3px;
  background: var(--brand-gradient);
}

.input,
.select {
  background: var(--surface-sunken);
  border-color: var(--line-strong);
  color: var(--ink-strong);
}
.input:hover,
.select:hover {
  border-color: #4d4d58;
}
.input:focus,
.select:focus {
  border-color: var(--tt-red);
  box-shadow: 0 0 0 3px rgba(254, 44, 85, 0.18);
}
.input[readonly] {
  color: var(--ink-muted);
}

.check input[type="checkbox"] {
  accent-color: var(--tt-red);
}

:where(button, a, input, select, textarea, [tabindex]):focus-visible {
  outline-color: var(--tt-red);
}
::selection {
  background: rgba(254, 44, 85, 0.3);
}

.table tbody tr:hover {
  background: rgba(255, 255, 255, 0.04);
}

.video-card {
  background: var(--surface-sunken);
}
.video-card:hover,
.gift-tile:hover,
.deck-add:hover {
  border-color: var(--tt-red);
}
.gift-tile {
  background: var(--surface-sunken);
}
.gift-tile:hover {
  background: #2e2e35;
  box-shadow: none;
}
.deck-add:hover {
  color: var(--tt-red);
  background: rgba(254, 44, 85, 0.08);
}

.deck-card {
  background: var(--surface-sunken);
}
.deck-mini-select {
  background: var(--surface-card);
  border-color: var(--line);
  color: var(--ink);
}
.deck-mini-select:focus {
  border-color: var(--tt-red);
}

.seg-btn {
  background: var(--surface-card);
}
.seg-btn:hover:not(.is-active) {
  background: rgba(255, 255, 255, 0.07);
  color: #fff;
}
.seg-btn.is-active {
  background: var(--tt-red);
}
.swatch-btn.is-active {
  box-shadow: 0 0 0 2px var(--surface-card), 0 0 0 4px var(--tt-red);
}

*::-webkit-scrollbar-thumb {
  background: #3a3a42;
}
*::-webkit-scrollbar-thumb:hover {
  background: #55555f;
}

/* The gift picker, laid out like TikTok's gift bar: artwork, name, then the
   price on a coin. */
.gift-coin {
  display: inline-flex;
  align-items: center;
  gap: 3px;
  font-size: 10px;
  font-weight: 600;
  color: var(--ink-muted);
}
.gift-coin::before {
  content: "";
  width: 11px;
  height: 11px;
  border-radius: 999px;
  background: linear-gradient(135deg, #ffd66b, #f0a93c);
  flex-shrink: 0;
}

/* Inline code sits at the same weight as body copy on dark, so it needs its own
   colour to read as a literal value you type rather than as prose. */
code {
  color: var(--tt-cyan);
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: 0.92em;
}

/* Header bars: on dark, a card and a header both sitting on near-black need a
   line between them or the page has no structure. */
header {
  background: var(--surface-card) !important;
  border-color: var(--line) !important;
}

/* ---------------------------------------------------------------------------
   Two reds, on purpose
   ---------------------------------------------------------------------------
   TikTok's #fe2c55 carries white text at only 3.68:1 — under the 4.5:1 this
   app holds itself to for label-sized text. So the exact brand red is kept for
   everything that does not have to be read through: rings, the active nav
   label, focus outlines, the slider. Anything filled with white text on top
   drops to a slightly deeper red that hits 4.64:1 and is still unmistakably
   the same colour.
   --------------------------------------------------------------------------- */
:root {
  --tt-red-fill: #e51447;
  --tt-red-fill-hover: #f52350;
}

.btn-primary {
  background: var(--tt-red-fill);
}
.btn-primary:hover:not(:disabled) {
  background: var(--tt-red-fill-hover);
}
.nav-cta {
  background: var(--tt-red-fill) !important;
}
.nav-cta:hover {
  background: var(--tt-red-fill-hover) !important;
}
.seg-btn.is-active {
  background: var(--tt-red-fill);
}
.tt-live-tag,
.nav-live-tag {
  background: var(--tt-red-fill);
}

/* ===========================================================================
   Khung vinh danh hạng 1 — phoenix avatar frame
   ---------------------------------------------------------------------------
   The source was a Spine 3.8 skeletal project. It has been baked here into a
   single looping APNG — 40 frames of the 2.67s animation at 15fps — so the
   overlay needs no Spine runtime and no JavaScript to play it: an APNG animates
   on its own as a CSS background.
   The ring's opening is padded to dead centre of a 460px canvas, so the avatar
   sits at 50%/50% and the frame scales around it by one ratio. The opening is
   220px wide but only 203px tall, so the ratio comes from the tighter dimension
   plus a little margin: 460/203 = 2.27 would just touch, 2.38 keeps the ring's
   inner edge visible all the way round.
   =========================================================================== */

.rank-frame {
  position: relative;
  flex-shrink: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  /* Set --avatar to the avatar diameter; everything else follows from it. */
  width: var(--avatar, 28px);
  height: var(--avatar, 28px);
}

.rank-frame > .tt-avatar {
  width: 100%;
  height: 100%;
  box-shadow: none;
}

.rank-frame::after {
  content: "";
  position: absolute;
  left: 50%;
  top: 50%;
  width: calc(var(--avatar, 28px) * 2.38);
  height: calc(var(--avatar, 28px) * 2.38);
  transform: translate(-50%, -50%);
  /* The artwork animates itself, so there is no CSS motion here — the flames,
     sparks and the phoenix's drift are all in the baked frames. The static PNG
     stays as the first layer so something sensible shows if the APNG is ever
     unavailable. */
  background: url("/shared/frames/top1-anim.png") center / contain no-repeat,
              url("/shared/frames/top1.png") center / contain no-repeat;
  pointer-events: none;
}

/* A warm halo under the avatar, pulsing on the same clock as the frame */
.rank-frame::before {
  content: "";
  position: absolute;
  left: 50%;
  top: 50%;
  width: calc(var(--avatar, 28px) * 1.75);
  height: calc(var(--avatar, 28px) * 1.75);
  transform: translate(-50%, -50%);
  border-radius: 999px;
  /* A quiet, unmoving warmth under the avatar. The ring above supplies all the
     movement; a second pulsing thing behind it only muddies the effect. */
  background: radial-gradient(circle, rgba(255, 130, 50, 0.3), rgba(255, 90, 40, 0) 70%);
  pointer-events: none;
}

/* The gold bar already marks rank 1; inside it the frame needs room to breathe
   or its wings clip the row above and below. */
.tt-rank.is-top {
  padding-top: 12px;
  padding-bottom: 12px;
}

/* ==========================================================================
   Trạng thái tải — thay cho chớp sáng
   ==========================================================================

   Vì sao cần cả khối này: Tailwind ở đây là bản chạy trong trình duyệt
   (public/shared/tailwind.js), nó sinh CSS SAU khi trang đã parse. Trong
   khoảnh khắc đó mọi class layout chưa có style, và <html> vốn không có nền
   nên trình duyệt vẽ trắng mặc định — đó chính là cú chớp sáng.

   Hai lớp phòng thủ:
     1. <html> có nền tối ngay từ đầu (thêm cả một <style> nội tuyến ở đầu
        mỗi trang, để có màu trước khi file này kịp tải).
     2. Một màn chờ phủ kín, tự mờ đi khi trang đã sẵn sàng. */

html {
  background: var(--surface-app);
}

#app-loading {
  position: fixed;
  inset: 0;
  z-index: 9999;
  display: flex;
  align-items: center;
  justify-content: center;
  background: var(--surface-app);
  transition: opacity 280ms ease;
}
#app-loading.done {
  opacity: 0;
  pointer-events: none;
}
#app-loading .ring {
  width: 46px;
  height: 46px;
  border-radius: 50%;
  border: 3px solid rgba(255, 255, 255, 0.12);
  border-top-color: var(--tt-red);
  animation: app-spin 800ms linear infinite;
}

@keyframes app-spin {
  to {
    transform: rotate(360deg);
  }
}

/* Khối xám nhấp nháy đứng thay chỗ dữ liệu đang tải. Dùng nó thay vì để trống
   rồi đổ dữ liệu vào: danh sách rỗng nhấp một cái rồi đầy lên chính là kiểu
   chớp mà người dùng thấy khó chịu. */
.skel {
  position: relative;
  overflow: hidden;
  border-radius: 10px;
  background: var(--surface-sunken, #17171c);
}
.skel::after {
  content: "";
  position: absolute;
  inset: 0;
  background: linear-gradient(
    100deg,
    transparent 20%,
    rgba(255, 255, 255, 0.07) 50%,
    transparent 80%
  );
  background-size: 220% 100%;
  animation: skel-shimmer 1200ms linear infinite;
}
.skel-row {
  height: 44px;
  margin-bottom: 8px;
}
.skel-card {
  aspect-ratio: 1 / 1;
}
.skel-line {
  height: 12px;
  margin-bottom: 8px;
}

@keyframes skel-shimmer {
  from {
    background-position: 160% 0;
  }
  to {
    background-position: -60% 0;
  }
}

/* Dữ liệu thật hiện ra bằng cách mờ dần lên, không bật đột ngột. */
.fade-in {
  animation: fade-in 220ms ease both;
}

@keyframes fade-in {
  from {
    opacity: 0;
    transform: translateY(3px);
  }
  to {
    opacity: 1;
    transform: none;
  }
}

@media (prefers-reduced-motion: reduce) {
  #app-loading .ring {
    animation-duration: 2400ms;
  }
  .skel::after {
    animation: none;
  }
  .fade-in {
    animation: none;
  }
}

/* Ô xem trước trong thư viện video.
   Nền carô chứ không phải đen đặc: video hiệu ứng cho OBS hầu hết có nền trong
   suốt, đặt trên nền đen thì phần trong suốt lẫn luôn vào nền và ô trông như
   hỏng. Carô là quy ước quen thuộc cho "chỗ này trong suốt". */
/* Ô xem trước clip hiệu ứng.

   9:16 cho khớp với video dọc — định dạng của gần như mọi clip hiệu ứng TikTok.
   Video 1080x1920 lấp kín ô; clip nào tỉ lệ khác thì object-contain vẫn hiện
   trọn khung, chỉ là có viền.

   Nền KHÔNG còn là ô ca-rô. Clip hiệu ứng gần như luôn trong suốt, mà ca-rô thì
   chỉ nói được "chỗ này trong suốt" chứ không cho biết trên sóng nó sẽ ra sao.
   Lớp đầu là ảnh người dùng tự đặt (data/preview-bg.png); không có thì rơi
   xuống lớp dự phòng bên dưới — một màn hình LIVE tối có chữ chìm, đủ để thấy
   phần trong suốt và phần sáng của clip.

   Cả hai lớp đều hiện TRỌN VẸN chứ không bị cắt:

   • Ảnh tự đặt dùng "contain". Ô xem trước là khung dọc 9:16, còn ảnh chụp màn
     hình LIVE thường là khung ngang — "cover" phóng ảnh cho lấp kín ô rồi xén
     mất hai đầu, nên chỉ còn thấy một mẩu giữa ảnh. "contain" thu cả ảnh vừa
     vào ô; phần thừa để lộ màu nền đặc bên dưới.
   • Chữ chìm dự phòng co theo bề ngang ô và chỉ vẽ MỘT lần ở giữa. Trước đây nó
     là hoa văn lát 150x96 lặp lại, mà ô chỉ rộng ~95px nên dòng chữ luôn bị cắt
     cụt ở hai mép — nhìn ra vài chữ cái rời rạc chứ không thành tên. Cỡ chữ
     trong SVG được nâng lên cho vừa gần hết bề ngang: nó không còn được phóng
     to bằng cách lát ra ngoài khung nữa. */
.video-thumb {
  aspect-ratio: 9 / 16;
  background-color: #0b0b0f;
  background-image:
    url("/preview-bg"),
    url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='150' height='96'%3E%3Ctext x='75' y='58' font-family='Arial,sans-serif' font-size='17' fill='%23ffffff' fill-opacity='0.12' text-anchor='middle'%3EHUU TICH SHOP%3C/text%3E%3C/svg%3E"),
    linear-gradient(170deg, #191922 0%, #101017 45%, #07070b 100%);
  background-size: contain, 100% auto, cover;
  background-position: center, center, center;
  background-repeat: no-repeat, no-repeat, no-repeat;
}

/* Nút xoá thu vào góc, chỉ rõ lên khi trỏ vào ô — để ô giữ được kích thước nhỏ
   mà vẫn xoá được. */
.video-del {
  position: absolute;
  top: 4px;
  right: 4px;
  width: 22px;
  height: 22px;
  display: flex;
  align-items: center;
  justify-content: center;
  border-radius: 999px;
  border: none;
  cursor: pointer;
  font-size: 14px;
  line-height: 1;
  color: #fff;
  background: rgba(0, 0, 0, 0.55);
  opacity: 0;
  transition: opacity var(--fast, 120ms) var(--ease, ease), background var(--fast, 120ms) var(--ease, ease);
}
.video-card:hover .video-del,
.video-del:focus-visible {
  opacity: 1;
}
.video-del:hover {
  background: var(--danger, #e5484d);
}

/* Vùng thả file: cả thẻ thư viện đều nhận, không bắt người dùng nhắm vào một ô
   nhỏ. Viền nét đứt chỉ hiện lúc đang kéo file qua. */
.drop-active {
  outline: 2px dashed var(--tt-red);
  outline-offset: -6px;
}
.drop-hint {
  position: absolute;
  inset: 0;
  z-index: 5;
  display: none;
  align-items: center;
  justify-content: center;
  border-radius: inherit;
  background: rgba(254, 44, 85, 0.07);
  font-weight: 600;
  color: var(--tt-red);
  pointer-events: none;
}
.drop-active .drop-hint {
  display: flex;
}

/* Ô tạm trong lúc file đang được gửi lên. Đứng cùng lưới với video thật nên
   người dùng thấy ngay chỗ nó sắp xuất hiện. */
.upload-tile {
  /* Cung ti le voi o xem truoc, de luc tai xong o khong nhay kich thuoc. */
  aspect-ratio: 9 / 16;
  position: relative;
  overflow: hidden;
  border-radius: 8px;
  border: 1px solid var(--line);
  background: var(--surface-sunken, #17171c);
}
.upload-tile .up-name {
  position: absolute;
  left: 0;
  right: 0;
  bottom: 14px;
  padding: 0 4px;
  font-size: 10px;
  text-align: center;
  color: var(--ink-muted);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.upload-tile .up-pct {
  position: absolute;
  inset: 0;
  bottom: 26px;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 15px;
  font-weight: 700;
  color: var(--ink-strong);
}
.upload-tile .up-track {
  position: absolute;
  left: 6px;
  right: 6px;
  bottom: 6px;
  height: 4px;
  border-radius: 999px;
  background: rgba(255, 255, 255, 0.1);
  overflow: hidden;
}
.upload-tile .up-bar {
  height: 100%;
  width: 0%;
  border-radius: 999px;
  background: var(--tt-red);
  transition: width 160ms linear;
}

/* ===========================================================================
   Volume sliders
   ---------------------------------------------------------------------------
   Âm lượng xuất hiện ở ba chỗ — từng ô Stream Deck, từng luật kích hoạt, và
   mức tổng trong Bảng điều khiển — nên nó có kiểu dáng riêng ở đây thay vì ba
   lần dựng lại. Rãnh dày hơn mặc định của trình duyệt: người dùng kéo nó giữa
   buổi live, đôi khi trên một cửa sổ nhỏ đặt chồng lên OBS.
   =========================================================================== */

.vol-row {
  display: flex;
  align-items: center;
  gap: 9px;
}
.vol-icon {
  width: 20px;
  flex-shrink: 0;
  font-size: 15px;
  line-height: 1;
  text-align: center;
}
.vol-range {
  flex: 1;
  min-width: 0;
  height: 18px;
  appearance: none;
  -webkit-appearance: none;
  background: transparent;
  cursor: pointer;
}
.vol-range::-webkit-slider-runnable-track {
  height: 6px;
  border-radius: 999px;
  background: var(--surface-sunken);
  border: 1px solid var(--line-strong);
}
.vol-range::-moz-range-track {
  height: 6px;
  border-radius: 999px;
  background: var(--surface-sunken);
  border: 1px solid var(--line-strong);
}
.vol-range::-webkit-slider-thumb {
  -webkit-appearance: none;
  width: 15px;
  height: 15px;
  margin-top: -5px;
  border-radius: 999px;
  background: var(--brand-gradient);
  border: 2px solid var(--surface-card);
  box-shadow: var(--shadow-sm);
}
.vol-range::-moz-range-thumb {
  width: 15px;
  height: 15px;
  border-radius: 999px;
  border: 2px solid var(--surface-card);
  background: var(--brand-violet);
  box-shadow: var(--shadow-sm);
}
.vol-range:focus-visible { outline: none; }
.vol-range:focus-visible::-webkit-slider-thumb { box-shadow: 0 0 0 3px rgba(124, 92, 246, 0.5); }
.vol-range:focus-visible::-moz-range-thumb { box-shadow: 0 0 0 3px rgba(124, 92, 246, 0.5); }
/* Tắt tiếng hẳn là một trạng thái, không phải "âm lượng rất nhỏ" — nên nó đổi
   màu chứ không chỉ tụt về đầu rãnh. */
.vol-row.is-muted .vol-range::-webkit-slider-thumb { background: var(--danger); }
.vol-row.is-muted .vol-range::-moz-range-thumb { background: var(--danger); }

/* ===========================================================================
   Bảng điều khiển
   ---------------------------------------------------------------------------
   Cửa sổ nổi đặt chồng lên OBS, thường bị co lại còn một dải hẹp bên cạnh
   OBS. Nên ở đây không có tiêu đề mục nào: mỗi khối tự nói lên nó là gì bằng
   biểu tượng và bằng hình dạng của chính nó, còn từng dòng chữ giải thích đều
   là chỗ đã bị lấy khỏi lưới nút. Chữ ngắn tới mức đọc một nhịp là hiểu.
   =========================================================================== */

.ctl-block {
  border-top: 1px solid var(--line);
  padding: 8px 9px;
}
.ctl-block:first-child { border-top: none; }
/* Tài khoản khi nằm trên thanh công cụ: thấp bằng đúng mấy nút cạnh nó, viền
   mảnh, và co lại được để ổ khoá không bao giờ bị đẩy ra khỏi thanh. */
.ctl-account-bar {
  width: auto;
  min-width: 0;
  flex: 0 1 auto;
  height: 24px;
  padding: 0 7px 0 3px;
  gap: 5px;
  border-radius: 999px;
  background: var(--surface-sunken);
}
.ctl-account-bar .ctl-account-avatar { width: 18px; height: 18px; font-size: 10px; box-shadow: 0 0 0 1.5px var(--line-strong); }
.ctl-account-bar.is-live .ctl-account-avatar { box-shadow: 0 0 0 1.5px var(--ok); }
.ctl-account-bar .ctl-account-name { font-size: 11px; }
.ctl-account-bar .ctl-account-action { font-size: 9px; }
/* Khối đi liền ngay sau một tiêu đề thì không cần vạch ngăn — tiêu đề và phần
   điều khiển của nó là một cụm. */
.ctl-block-tight { border-top: none; padding-top: 0; }

/* Nhật ký ăn hết chỗ còn thừa của bảng thay vì bị chốt ở một chiều cao cố định.
   Chốt cứng thì cửa sổ cao sẽ để lại một mảng trống dưới đáy, còn cửa sổ thấp
   thì nhật ký bị cắt mất — đúng hai kiểu hỏng mà người dùng gặp. */
.ctl-block-log {
  flex: 1 1 auto;
  min-height: 96px;
  display: flex;
  flex-direction: column;
}
.ctl-block-log #ctl-log {
  flex: 1 1 auto;
  min-height: 0;
  overflow-y: auto;
}

/* Tiêu đề mục. Chỉ có ĐÚNG HAI cái trong cả bảng, cho hai thứ mà nhìn vào lưới
   nút thì không đoán ra được: đâu là video bấm tay, đâu là hiệu ứng tự động —
   hai thứ này phát ra hai overlay khác nhau trong OBS, nên nhầm là hỏng.
   Căn giữa và có nền riêng: chúng là vạch phân chia của cả bảng, không phải
   một cái nhãn nhỏ nép ở góc. */
.ctl-head {
  display: block;
  margin-bottom: 8px;
  padding: 4px 10px;
  border: 1px solid var(--line-strong);
  border-radius: 999px;
  background: var(--surface-sunken);
  font-size: 10px;
  font-weight: 800;
  letter-spacing: 0.1em;
  text-align: center;
  color: var(--ink-strong);
}

/* Hai nút cạnh nhau, chia đều bề ngang */
.ctl-row { display: flex; align-items: center; gap: 6px; }
.ctl-row > .ctl-btn { flex: 1; min-width: 0; }

/* Con số bám mép phải của thanh trượt, chữ số đều bề ngang để nó không giật
   khi giá trị đổi từ 9 sang 10. */
.ctl-num {
  flex-shrink: 0;
  min-width: 34px;
  text-align: right;
  font-size: 11px;
  font-weight: 700;
  color: var(--ink-muted);
  font-variant-numeric: tabular-nums;
}

/* Nút thấp và gọn, nhưng vẫn đủ cao để bấm trúng khi đang nhìn màn hình khác. */
.ctl-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 5px;
  height: 30px;
  padding: 0 10px;
  border-radius: 9px;
  border: 1px solid var(--line-strong);
  background: var(--surface-sunken);
  color: var(--ink);
  font: inherit;
  font-size: 12px;
  font-weight: 700;
  white-space: nowrap;
  cursor: pointer;
  transition: background-color var(--normal) var(--ease),
              border-color var(--normal) var(--ease),
              color var(--normal) var(--ease),
              transform var(--fast) var(--ease);
}
.ctl-btn:hover:not(:disabled) { color: #fff; border-color: var(--ink-muted); }
.ctl-btn:active:not(:disabled) { transform: scale(0.97); }
/* Nút đã hết việc thì mờ đi hẳn — trạng thái của cả khối nằm ở chỗ nút nào còn
   bấm được, nên không cần thêm một dòng chữ nói lại điều đó. */
.ctl-btn:disabled { opacity: 0.4; cursor: default; }
.ctl-btn.is-on { background: var(--ok-soft); border-color: var(--ok); color: var(--ok); }
.ctl-btn.is-off { background: var(--warn-soft); border-color: var(--warn); color: var(--warn); }

/* Thanh công cụ mảnh của nhật ký: lọc và xoá, không tiêu đề. */
.ctl-bar { display: flex; align-items: center; gap: 4px; margin-bottom: 6px; }
.ctl-bar .ctl-spacer { flex: 1; }

.ctl-chip {
  padding: 3px 9px;
  border-radius: 999px;
  border: 1px solid var(--line-strong);
  background: transparent;
  color: var(--ink-muted);
  font: inherit;
  font-size: 10px;
  font-weight: 700;
  line-height: 1.4;
  cursor: pointer;
  transition: color var(--normal) var(--ease), border-color var(--normal) var(--ease),
              background-color var(--normal) var(--ease);
}
.ctl-chip:hover { color: var(--ink); }
.ctl-chip.is-active { color: var(--ink-strong); border-color: var(--brand-violet); }
.ctl-chip-clear:hover { background: var(--danger); border-color: var(--danger); color: #fff; }

/* Một dòng nhật ký sự kiện quà, kèm nút × ở cuối để người dùng dọn tay. */
.ctl-log { display: flex; flex-direction: column; gap: 4px; }
.ctl-log-row {
  display: flex;
  align-items: center;
  gap: 7px;
  padding: 5px 4px 5px 8px;
  border-radius: 8px;
  background: var(--tt-chip);
  color: var(--tt-text);
  font-size: 12px;
  line-height: 1.25;
  transition: background-color var(--normal) var(--ease),
              box-shadow var(--normal) var(--ease);
}
/* Mỗi dòng là một hàng của cùng một bảng: mặt người tặng, tên, món quà, số
   lượng, giờ, rồi nút cắt. Mọi ô trừ tên đều rộng cố định, nên khi mười dòng
   xếp lên nhau thì các cột ảnh quà thẳng một mạch từ trên xuống — mắt chỉ cần
   quét dọc một cột là biết đang có những món gì. Tên là ô duy nhất co giãn, và
   là ô duy nhất được phép cắt bằng dấu ba chấm. */
.ctl-log-row .ctl-log-avatar {
  flex-shrink: 0;
  width: 22px;
  height: 22px;
  border-radius: 999px;
  overflow: hidden;
  background: var(--surface-sunken);
  display: flex;
  align-items: center;
  justify-content: center;
}
.ctl-log-row .ctl-log-avatar img { width: 100%; height: 100%; object-fit: cover; display: block; }
.ctl-log-row .ctl-log-initial {
  font-size: 11px;
  font-weight: 800;
  color: var(--ink-muted);
  line-height: 1;
}
.ctl-log-row .ctl-log-who {
  min-width: 0;
  flex: 1;
  font-weight: 700;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
/* Ảnh quà — cột thẳng hàng. Hộp cố định 22px và ảnh nằm vừa trong đó, nên một
   món quà nghệ thuật cao gấp đôi món khác cũng không làm dòng đó phình ra. */
.ctl-log-row .ctl-log-gift {
  flex-shrink: 0;
  width: 22px;
  height: 22px;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 13px;
  line-height: 1;
}
.ctl-log-row .ctl-log-gift img {
  max-width: 100%;
  max-height: 100%;
  object-fit: contain;
  display: block;
}
/* Số combo: rộng cố định và chữ số đều bề ngang, để cột giờ bên cạnh không xê
   dịch giữa ×9 và ×10. Trống khi chỉ tặng một cái. */
.ctl-log-row .ctl-log-count {
  flex-shrink: 0;
  min-width: 26px;
  text-align: right;
  font-size: 11px;
  font-weight: 800;
  color: var(--tt-text);
  font-variant-numeric: tabular-nums;
}
.ctl-log-row.is-playing .ctl-log-count { color: var(--tt-pink-soft); }
.ctl-log-row .ctl-log-time {
  flex-shrink: 0;
  font-size: 10px;
  color: var(--tt-text-dim);
  font-variant-numeric: tabular-nums;
}

/* Mốc giá trị: 100 / 1.000 / 10.000 kim cương.
   Một buổi live chạy hàng trăm đóa hoa hồng 1 kim cương, và giữa dòng đó một
   món 10.000 không được phép trôi qua trông y như mọi dòng khác. Mỗi mốc đổi
   nền và viền — CHỈ MÀU, không chuyển động. Chuyển động ở đây dành riêng cho
   "đang phát"; nếu mốc giá cũng nhấp nháy thì hai thứ tranh nhau, mà thứ người
   vận hành cần thấy trước là cái gì đang trên sóng. */
.ctl-log-row {
  position: relative;
  border: 2px solid transparent;
  min-height: 34px;
}

/* Từ 100: xanh ngọc của TikTok, đủ để tách khỏi nền xám. */
.ctl-log-row.tier-1 {
  background: rgba(37, 244, 238, 0.10);
  border-color: rgba(37, 244, 238, 0.45);
}
/* Từ 1.000: vàng kim, và chữ số combo cũng ăn theo. */
.ctl-log-row.tier-2 {
  background: linear-gradient(90deg, rgba(242, 193, 78, 0.22), rgba(242, 193, 78, 0.08));
  border-color: var(--tt-gold-2);
}
.ctl-log-row.tier-2 .ctl-log-count { color: var(--tt-gold-edge); }
/* Từ 10.000: vàng đậm, viền sáng nhất, thêm quầng sáng ra ngoài. */
.ctl-log-row.tier-3 {
  background: linear-gradient(90deg, rgba(201, 138, 27, 0.48), rgba(242, 193, 78, 0.18));
  border-color: var(--tt-gold-edge);
  box-shadow: 0 0 12px rgba(242, 193, 78, 0.30);
}
.ctl-log-row.tier-3 .ctl-log-count { color: #fff; }

/* Quà này ĐANG có hiệu ứng trên sóng: một vệt sáng chạy vòng quanh khung.
   Đây là thứ duy nhất trên bảng có chuyển động, nên mắt bắt được ngay cả khi
   đang nhìn chỗ khác — mà nó vẫn không che mất màu của mốc giá, vì vệt sáng
   nằm trên VIỀN còn mốc giá nằm ở NỀN.

   Cách dựng: một hình nón màu xoay tròn, rồi khoét ruột bằng mask để chỉ còn
   lại đúng cái vành. Góc xoay là một biến đã đăng ký qua @property — không
   đăng ký thì trình duyệt coi nó là chuỗi và không nội suy được, vệt sáng sẽ
   nhảy giật từng nấc thay vì chạy mượt. */
@property --ctl-ang {
  syntax: "<angle>";
  initial-value: 0deg;
  inherits: false;
}

.ctl-log-row.is-playing {
  border-color: transparent;
}
.ctl-log-row.is-playing::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 0;
  border-radius: inherit;
  padding: 3px;
  /* Nền hồng đứng yên bên dưới, cộng HAI vệt sáng đối xứng chạy vòng quanh —
     vành lúc nào cũng thấy được, và luôn có một vệt trong tầm mắt dù nhìn vào
     cạnh nào của dòng. */
  background: conic-gradient(
    from var(--ctl-ang),
    #ff3f87 0turn,
    #fff 0.06turn,
    #ffb3d6 0.13turn,
    #ff3f87 0.25turn,
    #ff3f87 0.5turn,
    #fff 0.56turn,
    #ffb3d6 0.63turn,
    #ff3f87 0.75turn,
    #ff3f87 1turn
  );
  -webkit-mask:
    linear-gradient(#000 0 0) content-box,
    linear-gradient(#000 0 0);
  -webkit-mask-composite: xor;
  mask:
    linear-gradient(#000 0 0) content-box,
    linear-gradient(#000 0 0);
  mask-composite: exclude;
  animation: ctl-run 1.2s linear infinite;
  pointer-events: none;
}

/* Quầng sáng thở theo, toả ra NGOÀI khung. Vành chạy nói "cái này đang sống",
   còn quầng thở kéo mắt về phía nó từ bên kia màn hình. */
.ctl-log-row.is-playing::after {
  content: "";
  position: absolute;
  inset: -1px;
  z-index: -1;
  border-radius: inherit;
  box-shadow: 0 0 14px 2px rgba(255, 63, 135, 0.55);
  animation: ctl-breathe 1.2s ease-in-out infinite;
  pointer-events: none;
}
@keyframes ctl-breathe {
  50% { box-shadow: 0 0 22px 5px rgba(255, 63, 135, 0.8); }
}
/* Nội dung phải nằm trên cái vành, nếu không mép sáng sẽ liếm vào mặt người tặng. */
.ctl-log-row.is-playing > * {
  position: relative;
  z-index: 1;
}
/* Nền đậm hơn hẳn cho dòng đang phát, và chữ trắng lên — để nó không chỉ khác ở
   cái viền mà khác ở cả mảng màu. */
.ctl-log-row.is-playing {
  background: linear-gradient(90deg, rgba(255, 63, 135, 0.34), rgba(255, 63, 135, 0.12));
}
.ctl-log-row.is-playing .ctl-log-who { color: #fff; }
.ctl-log-row.is-playing .ctl-log-count { color: #fff; }
/* Ảnh quà nhô lên một chút và sáng hơn khi hiệu ứng của nó đang chạy. */
.ctl-log-row.is-playing .ctl-log-gift {
  transform: scale(1.14);
  filter: drop-shadow(0 0 5px rgba(255, 140, 200, 0.9));
}

@keyframes ctl-run {
  to { --ctl-ang: 360deg; }
}

/* Máy nào tắt hiệu ứng chuyển động thì vành đứng yên nhưng vẫn hồng — mất
   chuyển động chứ không mất thông tin. */
@media (prefers-reduced-motion: reduce) {
  .ctl-log-row.is-playing::before,
  .ctl-log-row.is-playing::after { animation: none; }
}

/* Vòng sáng quanh mặt người tặng, giống hệt vòng trên ô Deck đang phát. */
.ctl-log-row.is-playing .ctl-log-avatar { box-shadow: 0 0 0 2px var(--tt-pink); }

/* Nút xoá luôn hiện và luôn đỏ, không chỉ khi rê chuột: bảng này còn được dán
   vào dock của OBS, nơi cảm ứng và bút không tạo ra trạng thái hover nào cả —
   một nút chỉ đỏ lúc hover thì ở đó nó không bao giờ đỏ. Đỏ sẵn cũng là lời
   nhắc rằng trên dòng đang phát, cú bấm này cắt sóng thật. */
.ctl-log-x {
  flex-shrink: 0;
  width: 20px;
  height: 20px;
  display: flex;
  align-items: center;
  justify-content: center;
  border: 1px solid rgba(255, 74, 95, 0.45);
  border-radius: 6px;
  background: var(--danger-soft);
  color: var(--danger);
  font-size: 14px;
  font-weight: 700;
  line-height: 1;
  cursor: pointer;
  transition: background-color var(--normal) var(--ease), color var(--normal) var(--ease),
              border-color var(--normal) var(--ease);
}
.ctl-log-x:hover { background: var(--danger); border-color: var(--danger); color: #fff; }

/* Tài khoản TikTok đang được mượn để đọc dữ liệu: một dòng, cả khối là một nút. */
.ctl-account {
  display: flex;
  align-items: center;
  gap: 7px;
  width: 100%;
  padding: 5px 7px;
  border-radius: 9px;
  border: 1px solid var(--line);
  background: var(--surface-sunken);
  color: var(--ink);
  font: inherit;
  text-align: left;
  cursor: pointer;
  transition: border-color var(--normal) var(--ease);
}
.ctl-account:hover { border-color: var(--ink-muted); }
.ctl-account:disabled { cursor: default; opacity: 0.7; }
.ctl-account:disabled:hover { border-color: var(--line); }
.ctl-account-avatar {
  width: 22px;
  height: 22px;
  border-radius: 999px;
  flex-shrink: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 12px;
  background: var(--surface-card);
  box-shadow: 0 0 0 2px var(--line-strong);
}
.ctl-account.is-live .ctl-account-avatar { box-shadow: 0 0 0 2px var(--ok); }
.ctl-account-name {
  min-width: 0;
  flex: 1;
  font-size: 12px;
  font-weight: 700;
  color: var(--ink-strong);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.ctl-account-action { flex-shrink: 0; font-size: 10px; font-weight: 700; color: var(--ink-muted); }
.ctl-account:hover .ctl-account-action { color: var(--danger); }

.ctl-empty {
  padding: 6px 2px;
  text-align: center;
  font-size: 11px;
  color: var(--ink-muted);
}

/* ===========================================================================
   Ô Stream Deck: ảnh chiếm trọn ô, thiết lập nổi đè lên đáy
   =========================================================================== */

/* Lớp phủ mờ, không phải tấm che kín: chỉ đủ tối để chữ 10px đọc được, còn ảnh
   phía sau vẫn nhận ra. Một dải chuyển dần lên trên thay vì một mảng đặc đều —
   mép trên của tấm che mà là một đường thẳng thì nó cắt ngang ảnh trông rất
   thô. Thêm chút làm mờ hậu cảnh vì trên ảnh nhiều chi tiết, chỉ tối đi thôi
   thì chữ vẫn lẫn vào nền. */
.deck-tools {
  background: linear-gradient(
    180deg,
    rgba(0, 0, 0, 0) 0%,
    rgba(0, 0, 0, 0.30) 22%,
    rgba(0, 0, 0, 0.42) 100%
  );
  backdrop-filter: blur(3px) saturate(0.9);
  -webkit-backdrop-filter: blur(3px) saturate(0.9);
  padding-top: 12px;
}
.deck-tools .slot-name {
  color: #fff;
  text-shadow: 0 1px 3px rgba(0, 0, 0, 0.9);
}
/* Điều khiển cũng trong suốt theo, nếu không ba cái hộp đặc sẽ dựng lại đúng
   mảng che mà lớp phủ vừa tránh. */
.deck-tools .deck-mini-select {
  background: rgba(12, 12, 16, 0.55);
  border-color: rgba(255, 255, 255, 0.22);
  color: #fff;
}
.deck-tools .deck-mini-select:hover {
  background: rgba(12, 12, 16, 0.78);
  border-color: rgba(255, 255, 255, 0.4);
}
.deck-tools .deck-mini-danger {
  color: #ff8a99;
  text-shadow: 0 1px 2px rgba(0, 0, 0, 0.8);
}

/* Ô chờ trong lúc video chưa tua tới khung hình đại diện. */
.tile-loading {
  position: absolute;
  inset: 0;
  z-index: 1;
  background: linear-gradient(
      100deg,
      rgba(255, 255, 255, 0) 35%,
      rgba(255, 255, 255, 0.13) 50%,
      rgba(255, 255, 255, 0) 65%
    ),
    var(--surface-sunken);
  background-size: 250% 100%, 100% 100%;
  animation: tile-loading-sweep 1.1s linear infinite;
  pointer-events: none;
}
@keyframes tile-loading-sweep {
  from { background-position: 150% 0, 0 0; }
  to { background-position: -150% 0, 0 0; }
}
@media (prefers-reduced-motion: reduce) {
  .tile-loading { animation: none; }
}

/* ===========================================================================
   Viên kim cương
   ---------------------------------------------------------------------------
   Emoji 💎 luôn ra màu xanh và không đổi được — mà nó đứng cạnh vàng kim của
   các mốc giá trị nên trông lạc hẳn. Vẽ lại bằng SVG nhúng: cùng hình khối,
   nhưng vàng cam, và ở máy nào cũng ra đúng một kiểu.
   =========================================================================== */
.dia {
  display: inline-block;
  width: 0.95em;
  height: 0.95em;
  vertical-align: -0.12em;
  flex-shrink: 0;
  background: url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cdefs%3E%3ClinearGradient id='g' x1='0' y1='0' x2='0' y2='1'%3E%3Cstop offset='0' stop-color='%23ffe9a8'/%3E%3Cstop offset='.45' stop-color='%23ffc247'/%3E%3Cstop offset='1' stop-color='%23f08a1e'/%3E%3C/linearGradient%3E%3C/defs%3E%3Cpath fill='url(%23g)' d='M6.2 2.6h11.6l4.2 5.6L12 21.9 2 8.2z'/%3E%3Cpath fill='%23fff6d8' fill-opacity='.55' d='M6.2 2.6h11.6l1.7 5.6H4.5z'/%3E%3Cpath fill='%23000' fill-opacity='.13' d='M12 21.9 2 8.2h20z'/%3E%3C/svg%3E")
    center / contain no-repeat;
}

/* ===========================================================================
   Ô chọn quà để bắn thử
   ---------------------------------------------------------------------------
   Một nút duy nhất mang cả ảnh lẫn tên, thay cho cặp "ô chữ + nút Kho quà" cũ.
   Ô chữ đó chỉ đọc, nên nó giả vờ là chỗ nhập liệu mà không nhập được gì; còn
   người dùng nhận ra món quà qua HÌNH nhanh hơn hẳn qua tên.
   Cao đúng 40px bằng ô Số lượng và nút Bắn thử cạnh nó, nên cả hàng thẳng đáy.
   =========================================================================== */
.gift-pick {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  height: 40px;
  width: 260px;
  max-width: 100%;
  padding: 0 10px 0 5px;
  border: 1px solid var(--line-strong);
  border-radius: var(--radius-sm);
  background: var(--surface-card);
  color: var(--ink-strong);
  font: inherit;
  font-size: 13px;
  font-weight: 600;
  text-align: left;
  cursor: pointer;
  transition: border-color var(--normal) var(--ease), background-color var(--normal) var(--ease);
}
.gift-pick:hover { border-color: var(--brand-violet); }
.gift-pick img {
  width: 30px;
  height: 30px;
  object-fit: contain;
  flex-shrink: 0;
  border-radius: 7px;
  background: var(--surface-sunken);
}
/* Chưa chọn được quà nào — hộp rỗng có hình cái hộp quà mờ, để chỗ đó không
   thành một mảng trống trông như ảnh hỏng. */
.gift-pick img.is-empty {
  background: var(--surface-sunken)
    url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%238b8b96' stroke-width='1.6'%3E%3Crect x='3' y='8' width='18' height='12' rx='2'/%3E%3Cpath d='M3 12h18M12 8v12'/%3E%3Cpath d='M12 8S9.5 3.5 7.5 4.6 8.8 8 12 8zM12 8s2.5-4.5 4.5-3.4S15.2 8 12 8z'/%3E%3C/svg%3E")
    center / 18px no-repeat;
}
.gift-pick span:not(.gift-pick-caret) {
  flex: 1;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.gift-pick-caret { flex-shrink: 0; color: var(--ink-muted); font-size: 11px; }

/* Tổng giá trị: cao bằng những ô cùng hàng, chữ số đều bề ngang để nó không
   giật khi số lượng đổi. */
.combo-total {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  height: 40px;
  min-width: 96px;
  padding: 0 12px;
  border: 1px solid var(--line-strong);
  border-radius: var(--radius-sm);
  background: var(--surface-sunken);
  font-size: 14px;
  font-weight: 800;
  color: var(--ink-strong);
  font-variant-numeric: tabular-nums;
}
