/* ============================================================
   refinements.css — layered on top of styles.css.
   Kept as a separate file so the 190 KB base stylesheet stays
   untouched and these deltas are easy to review or revert.
   ============================================================ */

/* ============ 1. FAQ accordion ============
   <details> cannot animate its height, so faqView renders a button + a grid
   wrapper instead: 0fr → 1fr is a genuinely animatable value. */

.faq-item {
  padding: 0 !important;
  overflow: hidden;
  transition: border-color .3s ease, box-shadow .3s ease;
}
.faq-item.is-open {
  border-color: color-mix(in oklab, var(--primary) 34%, var(--border));
  box-shadow: 0 4px 18px -8px color-mix(in oklab, var(--primary) 40%, transparent);
}

.faq-q {
  display: flex; align-items: center; gap: 10px;
  width: 100%;
  padding: 15px 16px;
  border: 0; background: transparent;
  color: var(--text);
  font: inherit; font-weight: 600; font-size: 14.5px; line-height: 1.45;
  text-align: left; cursor: pointer;
  transition: color .2s ease;
}
.faq-q:hover { color: var(--ink-primary); }
.faq-q-text { flex: 1; min-width: 0; }
.faq-item.is-open .faq-q { color: var(--ink-primary); }

.faq-chev {
  flex-shrink: 0;
  color: var(--muted);
  transition: transform .42s cubic-bezier(.22,.9,.28,1), color .2s ease;
}
.faq-item.is-open .faq-chev { transform: rotate(180deg); color: var(--ink-primary); }

.faq-a {
  display: grid;
  grid-template-rows: 0fr;
  transition: grid-template-rows .46s cubic-bezier(.22,.9,.28,1);
}
.faq-item.is-open .faq-a { grid-template-rows: 1fr; }

.faq-a-inner {
  overflow: hidden;
  padding: 0 16px;
  line-height: 1.72;
  font-size: 14px;
  color: var(--text);
  white-space: pre-wrap;
  opacity: 0;
  transform: translateY(-6px);
  transition: opacity .3s ease .06s, transform .38s cubic-bezier(.22,.9,.28,1) .06s,
              padding-bottom .46s cubic-bezier(.22,.9,.28,1);
}
.faq-item.is-open .faq-a-inner {
  opacity: 1;
  transform: none;
  padding-bottom: 16px;
}

/* ============ 2. Forum votes — separate like / dislike counts ============ */

.qa-vote { display: flex; align-items: center; gap: 6px; }

.qa-vote-btn {
  display: inline-flex; align-items: center; gap: 5px;
  padding: 5px 11px 5px 9px;
  border: 1px solid var(--border);
  border-radius: 999px;
  background: var(--surface);
  color: var(--muted);
  cursor: pointer;
  font: inherit;
  transition: border-color .16s, background .16s, color .16s, transform .12s;
}
.qa-vote-btn:hover { border-color: var(--border-strong); color: var(--text); }
.qa-vote-btn:active { transform: scale(.95); }

.qa-vote-n {
  font-size: 12.5px; font-weight: 600;
  font-variant-numeric: tabular-nums;
  min-width: 9px; text-align: center;
}

.qa-vote-up.is-active {
  color: var(--success);
  border-color: color-mix(in oklab, var(--success) 45%, transparent);
  background: color-mix(in oklab, var(--success) 10%, var(--surface));
}
.qa-vote-down.is-active {
  color: var(--danger);
  border-color: color-mix(in oklab, var(--danger) 45%, transparent);
  background: color-mix(in oklab, var(--danger) 10%, var(--surface));
}

/* Tap acknowledgement on the number itself */
.qa-score-flip { animation: qaFlip .3s cubic-bezier(.2,.9,.3,1); }
@keyframes qaFlip {
  0%   { transform: translateY(-5px) scale(.85); opacity: .4; }
  100% { transform: translateY(0) scale(1); opacity: 1; }
}

/* ============ 3. Dashboard — rounder, softer cards and buttons ============ */

.dash-schedule-btn {
  border-radius: 18px !important;
  padding: 15px 18px !important;
  gap: 11px;
  font-weight: 650;
  border: 1px solid var(--border);
  background: linear-gradient(180deg,
              color-mix(in oklab, var(--surface) 100%, white 6%),
              color-mix(in oklab, var(--primary) 9%, var(--surface)));
  /* Raised-keycap depth, same language as the bottom nav / quick-tool tiles:
     a hard bottom edge for thickness, a soft outer shadow, an inset top
     highlight to catch the light. */
  box-shadow:
    0 3px 0 color-mix(in oklab, var(--primary) 25%, var(--shadow-ink) 12%),
    0 6px 16px -8px color-mix(in oklab, var(--shadow-ink) 22%, transparent),
    inset 0 1px 0 color-mix(in oklab, white 55%, transparent);
  transition: transform .16s cubic-bezier(.2,.9,.3,1), box-shadow .2s ease, border-color .2s ease;
}
.dash-schedule-btn:hover {
  transform: translateY(-2px);
  border-color: color-mix(in oklab, var(--primary) 32%, var(--border));
  box-shadow:
    0 5px 0 color-mix(in oklab, var(--primary) 25%, var(--shadow-ink) 12%),
    0 12px 26px -10px color-mix(in oklab, var(--primary) 50%, transparent),
    inset 0 1px 0 color-mix(in oklab, white 55%, transparent);
}
/* Pressed: sinks by exactly its own resting shadow height and the outer lift
   collapses into an inset dent - a real button push, not just a color change. */
.dash-schedule-btn:active {
  transform: translateY(3px);
  box-shadow: inset 0 2px 6px color-mix(in oklab, var(--shadow-ink) 25%, transparent);
}

.quick-grid { gap: 8px; }

.card.quick {
  border-radius: 14px !important;
  padding: 10px !important;
  transition: transform .16s cubic-bezier(.2,.9,.3,1), box-shadow .2s ease, border-color .2s ease;
}
.card.quick:hover {
  transform: translateY(-2px);
  border-color: color-mix(in oklab, var(--primary) 28%, var(--border));
  box-shadow: 0 10px 26px -14px rgba(0,0,0,.30);
}
.card.quick .qi {
  border-radius: 50%;
  width: 40px; height: 40px;
  display: grid; place-items: center;
  color: #af2828;
  background: color-mix(in oklab, var(--primary) 10%, transparent);
}
.card.quick h3 { font-size: 12px; }

/* Hero / block cards pick up the same rounding so the page reads as one set */
.next-event-card,
.dash-block-card,
.sem-progress-card,
.news-mini {
  border-radius: 18px !important;
}
.news-mini {
  transition: transform .16s cubic-bezier(.2,.9,.3,1), border-color .2s ease;
}
.news-mini:hover {
  transform: translateY(-1px);
  border-color: color-mix(in oklab, var(--primary) 26%, var(--border));
}

/* ============ 4. Layout safety — no accidental horizontal scroll ============
   `clip` (rather than `hidden`) does not turn body into a scroll container,
   so sticky headers and scroll-into-view keep working. */

body { overflow-x: clip; }
@supports not (overflow: clip) {
  body { overflow-x: hidden; }
}

img, svg, video, canvas { max-width: 100%; }

/* Long unbroken strings (URLs, emails) are the usual culprit behind a page
   that scrolls sideways on a phone. */
.card, .ai-bubble, .qa-text, .qa-comment-text, .comment-item, .faq-a-inner,
.news-summary, .news-title {
  overflow-wrap: anywhere;
}

/* Flex children that hold text need min-width:0 or they refuse to shrink. */
.card-row > *, .row > *, .header-inner > * { min-width: 0; }

/* Rows of chips / tabs scroll instead of wrapping into a tall stack. */
.chip-row, .prof-tabs, .news-chips {
  overflow-x: auto;
  scrollbar-width: none;
  -webkit-overflow-scrolling: touch;
}
.chip-row::-webkit-scrollbar,
.prof-tabs::-webkit-scrollbar,
.news-chips::-webkit-scrollbar { display: none; }

/* Any table stays inside its card. */
.compare-wrap, .table-wrap { overflow-x: auto; -webkit-overflow-scrolling: touch; }
table { max-width: 100%; }

/* The bottom tab bar must clear the home indicator on iOS. */
.mobile-tabs { padding-bottom: env(safe-area-inset-bottom); }

/* ============ 5. Small phones ============ */

@media (max-width: 400px) {
  .dash-schedule-btn { padding: 13px 15px !important; }
  .faq-q { padding: 13px 14px; font-size: 14px; }
  .faq-a-inner { padding: 0 14px; font-size: 13.5px; }
  .faq-item.is-open .faq-a-inner { padding-bottom: 14px; }
  .qa-vote-btn { padding: 5px 10px 5px 8px; }
}

/* ============ 6. Dark mode touch-ups ============ */

html[data-theme="dark"] .card.quick:hover {
  box-shadow: 0 10px 28px -12px rgba(0,0,0,.65);
}
html[data-theme="dark"] .dash-schedule-btn {
  background: linear-gradient(180deg,
              color-mix(in oklab, var(--surface) 100%, white 3%),
              color-mix(in oklab, var(--accent) 10%, var(--surface)));
  box-shadow:
    0 3px 0 color-mix(in oklab, var(--accent) 28%, var(--shadow-ink) 45%),
    0 6px 18px -8px color-mix(in oklab, var(--shadow-ink) 55%, transparent),
    inset 0 1px 0 color-mix(in oklab, white 8%, transparent);
}
html[data-theme="dark"] .dash-schedule-btn:hover {
  box-shadow:
    0 5px 0 color-mix(in oklab, var(--accent) 28%, var(--shadow-ink) 45%),
    0 12px 28px -10px rgba(0,0,0,.65),
    inset 0 1px 0 color-mix(in oklab, white 8%, transparent);
}
html[data-theme="dark"] .dash-schedule-btn:active {
  box-shadow: inset 0 2px 6px color-mix(in oklab, var(--shadow-ink) 55%, transparent);
}
html[data-theme="dark"] .faq-item.is-open {
  box-shadow: 0 4px 20px -8px rgba(0,0,0,.6);
}

/* ============ 7. Reduced motion ============ */

@media (prefers-reduced-motion: reduce) {
  .faq-a { transition: none; }
  .faq-a-inner, .faq-chev { transition: none; }
  .card.quick, .dash-schedule-btn, .news-mini,
  .qa-vote-btn { transition: none; }
  .card.quick:hover, .dash-schedule-btn:hover,
  .dash-schedule-btn:active,
  .news-mini:hover { transform: none; }
  .qa-score-flip { animation: none; }
}

/* ============ 8. Votes: like = blue, dislike = red ============ */

.qa-vote-up.is-active {
  color: #2563eb;
  border-color: color-mix(in oklab, #2563eb 48%, transparent);
  background: color-mix(in oklab, #2563eb 11%, var(--surface));
}
html[data-theme="dark"] .qa-vote-up.is-active {
  color: #60a5fa;
  border-color: color-mix(in oklab, #60a5fa 46%, transparent);
  background: color-mix(in oklab, #60a5fa 15%, var(--surface));
}
.qa-vote-down.is-active {
  color: #dc2626;
  border-color: color-mix(in oklab, #dc2626 48%, transparent);
  background: color-mix(in oklab, #dc2626 11%, var(--surface));
}
html[data-theme="dark"] .qa-vote-down.is-active {
  color: #f87171;
  border-color: color-mix(in oklab, #f87171 46%, transparent);
  background: color-mix(in oklab, #f87171 15%, var(--surface));
}
.qa-vote-up:hover   { color: #2563eb; border-color: color-mix(in oklab, #2563eb 34%, transparent); }
.qa-vote-down:hover { color: #dc2626; border-color: color-mix(in oklab, #dc2626 34%, transparent); }

/* ============ 9. No flashing scrollbar on navigation ============
   The view-enter animation changes the page height for ~450ms, which toggles
   the vertical scrollbar and shifts the whole layout sideways for a frame.
   Reserving the gutter permanently removes the flash. */

html {
  scrollbar-gutter: stable;
  overflow-y: scroll;         /* fallback where scrollbar-gutter is unsupported */
}
@supports (scrollbar-gutter: stable) {
  html { overflow-y: auto; }
}

/* Enter animations must never push content past the viewport edge. */
#app, #app.view-enter { overflow-x: clip; }
@supports not (overflow: clip) {
  #app, #app.view-enter { overflow-x: hidden; }
}

/* ============ 10. Header profile chip — initial only ============ */

.user-chip--compact {
  padding: 4px !important;
  gap: 4px;
  border-radius: 999px !important;
}
.user-chip--compact .user-dot { margin: 0; }
/* Belt-and-braces: if a cached bundle still renders the name, hide it. */
.user-chip--compact .user-name { display: none; }

/* ============ 11. Quick-tool cards stay compact ============ */

.card.quick .qt p {
  display: -webkit-box;
  -webkit-line-clamp: 1;
  line-clamp: 1;
  -webkit-box-orient: vertical;
  overflow: hidden;
  font-size: 11px;
  margin: 2px 0 0;
}
.card.quick .qt h3 { margin: 0; }
/* On a half-width mobile card even a single clamped line still gets cut
   mid-word ("საგნების...") — reads as unfinished. Drop it there entirely
   instead of clamping. This selector's specificity matches the rule above,
   so being later in the same file is what makes it win — a styles.css rule
   trying to override this from a different file would need to out-specify
   it, which is exactly what silently failed the first time this was tried. */
@media (max-width: 768px) {
  .card.quick .qt p { display: none; }
  /* With the description gone the tile only needs to hold an icon + a short
     label — shrink the whole card to match instead of leaving it sized for
     the two-line copy it no longer shows. */
  .card.quick { padding: 10px !important; border-radius: 14px !important; }
  .card.quick .qi { width: 28px; height: 28px; border-radius: 50%; }
  .card.quick .qi svg { width: 15px; height: 15px; }
  .card.quick h3 { font-size: 12px; }
  .quick-grid { gap: 8px; }
}

/* ============ 12. News feed ============ */

.nw-hero { margin: 4px 0 14px; }
.nw-h1 { margin: 0; font-size: clamp(19px, 4.5vw, 24px); font-weight: 700; letter-spacing: -.02em; }
.nw-sub { margin: 3px 0 0; font-size: 13px; color: var(--muted); }

/* Filters */
.nw-filters {
  display: flex; flex-direction: column; gap: 9px;
  padding: 11px 12px;
  margin-bottom: 16px;
  border: 1px solid var(--border);
  border-radius: 16px;
  background: var(--surface);
}
.nw-search {
  display: flex; align-items: center; gap: 8px;
  padding: 0 12px;
  border: 1px solid var(--border);
  border-radius: 999px;
  background: var(--bg);
  color: var(--muted);
  transition: border-color .16s, box-shadow .16s;
}
.nw-search:focus-within {
  border-color: var(--primary);
  box-shadow: 0 0 0 3px var(--ring);
  color: var(--ink-primary);
}
.nw-search input {
  flex: 1; min-width: 0;
  border: 0; outline: none; background: transparent;
  color: var(--text);
  font: inherit; font-size: 13.5px;
  padding: 9px 0;
}
.nw-search input::-webkit-search-cancel-button { cursor: pointer; }

.nw-chips { gap: 6px; padding-bottom: 2px; }
.nw-chips .chip { white-space: nowrap; font-size: 12.5px; }
.nw-chips .chip svg { flex-shrink: 0; color: var(--cat, currentColor); }
.nw-chips .chip.active {
  border-color: var(--primary);
  background: color-mix(in oklab, var(--primary) 12%, var(--surface));
  color: var(--ink-primary);
}
.nw-chips .chip.active svg { color: var(--ink-primary); }

.nw-filter-foot {
  display: flex; align-items: center; justify-content: space-between; gap: 10px;
  font-size: 11.5px;
}
.nw-saved-toggle {
  display: inline-flex; align-items: center; gap: 5px;
  padding: 5px 10px; border: 1px solid var(--border); border-radius: 999px;
  background: var(--surface); color: var(--muted);
  font-family: inherit; font-size: 12px; font-weight: 600; cursor: pointer;
  transition: color .15s ease, border-color .15s ease, background-color .15s ease;
}
.nw-saved-toggle:hover { color: var(--ink-primary); border-color: var(--primary); }
.nw-saved-toggle.is-active {
  color: var(--ink-primary); border-color: var(--primary);
  background: color-mix(in oklab, var(--primary) 10%, var(--surface));
}
.nw-saved-toggle svg { flex-shrink: 0; }

/* Date groups */
.nw-group + .nw-group { margin-top: 22px; }
.nw-group-head {
  margin: 0 0 9px;
  font-size: 11.5px; font-weight: 700;
  letter-spacing: .06em; text-transform: uppercase;
  color: var(--muted);
}
.nw-list { display: flex; flex-direction: column; gap: 8px; }

/* Item — the whole block is the link */
.nw-item {
  display: flex; gap: 12px;
  padding: 13px 14px;
  border: 1px solid var(--border);
  border-radius: 16px;
  background: var(--surface);
  color: inherit; text-decoration: none;
  transition: border-color .16s, transform .16s cubic-bezier(.2,.9,.3,1), box-shadow .18s;
}
a.nw-item:hover {
  transform: translateY(-2px);
  border-color: color-mix(in oklab, var(--primary) 30%, var(--border));
  box-shadow: 0 10px 24px -14px rgba(0,0,0,.30);
}
.nw-item.is-pinned {
  border-color: color-mix(in oklab, var(--primary) 30%, var(--border));
  background: color-mix(in oklab, var(--primary) 4%, var(--surface));
}

/* Date block — the fastest thing to scan down a feed */
.nw-date {
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  flex-shrink: 0;
  width: 44px; height: 46px;
  border-radius: 12px;
  background: var(--surface-2);
  line-height: 1.1;
}
html[data-theme="dark"] .nw-date { background: color-mix(in oklab, var(--text) 8%, transparent); }
.nw-date-d { font-size: 17px; font-weight: 700; font-variant-numeric: tabular-nums; }
.nw-date-m { font-size: 10.5px; color: var(--muted); }

.nw-body { display: flex; flex-direction: column; min-width: 0; flex: 1; gap: 3px; }

.nw-meta { display: flex; align-items: center; gap: 7px; flex-wrap: wrap; font-size: 11px; }
.nw-cat {
  display: inline-flex; align-items: center; gap: 4px;
  color: var(--cat, var(--primary));
  font-weight: 650;
}
.nw-cat svg { flex-shrink: 0; }
.nw-uni { color: var(--muted); }
.nw-pin {
  color: var(--ink-primary); font-weight: 650;
  padding: 1px 7px; border-radius: 999px;
  background: color-mix(in oklab, var(--primary) 12%, transparent);
}
.nw-new {
  color: var(--success); font-weight: 650;
  padding: 1px 7px; border-radius: 999px;
  background: color-mix(in oklab, var(--success) 12%, transparent);
}

.nw-title {
  font-size: 14.5px; font-weight: 650; line-height: 1.4;
  display: -webkit-box; -webkit-line-clamp: 2; line-clamp: 2;
  -webkit-box-orient: vertical; overflow: hidden;
}
.nw-sum {
  font-size: 12.5px; line-height: 1.5; color: var(--muted);
  display: -webkit-box; -webkit-line-clamp: 2; line-clamp: 2;
  -webkit-box-orient: vertical; overflow: hidden;
}
.nw-foot {
  display: flex; align-items: center; justify-content: space-between; gap: 10px;
  margin-top: 3px; font-size: 11px; color: var(--muted);
}
.nw-foot-trail { display: flex; align-items: center; gap: 8px; flex-shrink: 0; }
.nw-foot-actions { display: flex; align-items: center; gap: 2px; }
.nw-action-btn {
  display: inline-flex; align-items: center; justify-content: center;
  width: 24px; height: 24px; padding: 0; border: 0; border-radius: 50%;
  background: transparent; color: var(--muted); cursor: pointer;
  transition: color .15s ease, background-color .15s ease;
}
.nw-action-btn:hover { color: var(--ink-primary); background: color-mix(in oklab, var(--text) 8%, transparent); }
.nw-action-btn.is-saved { color: var(--ink-primary); }
.nw-go { color: var(--ink-primary); font-weight: 600; flex-shrink: 0; }

.nw-more { text-align: center; padding: 16px 0 4px; }
.nw-more .btn { font-size: 12.5px; padding: 7px 16px; border-radius: 999px; }

.nw-empty {
  display: flex; flex-direction: column; align-items: center;
  padding: 34px 16px; text-align: center;
}
.nw-empty-ico { display: flex; color: var(--muted); opacity: .5; margin-bottom: 8px; }
.nw-empty p { margin: 0; font-size: 13px; color: var(--muted); }

@media (max-width: 400px) {
  .nw-item { padding: 11px 12px; gap: 10px; }
  .nw-date { width: 40px; height: 42px; }
  .nw-date-d { font-size: 15.5px; }
  .nw-title { font-size: 14px; }
  .nw-sum { -webkit-line-clamp: 2; line-clamp: 2; font-size: 12px; }
}

@media (prefers-reduced-motion: reduce) {
  .nw-item { transition: none; }
  a.nw-item:hover { transform: none; }
}

.nw-rel { flex-shrink: 0; }

/* ============ 13. Forum list — subjects as panels ============
   One panel per row instead of a dense grid: a subject line is a tap target,
   and at 16px the title is readable at arm's length. */

.forums-grid {
  display: flex !important;
  flex-direction: column;
  gap: 9px;
}

.forum-card {
  padding: 0 !important;
  border-radius: 18px !important;
  overflow: hidden;
  transition: border-color .16s ease, box-shadow .18s ease, transform .14s cubic-bezier(.2,.9,.3,1);
}
.forum-card:hover {
  border-color: color-mix(in oklab, var(--primary) 30%, var(--border));
  box-shadow: 0 10px 26px -16px rgba(0,0,0,.32);
}

.forum-card-row { align-items: stretch; gap: 0; }

.forum-card-link {
  display: flex; align-items: center; gap: 13px;
  flex: 1; min-width: 0;
  padding: 15px 15px;
  text-decoration: none; color: inherit;
  -webkit-tap-highlight-color: transparent;
  transition: background .16s ease;
}
/* Press feedback — the panel dips under the finger, then springs back. */
.forum-card-link:active {
  background: color-mix(in oklab, var(--primary) 8%, transparent);
  transform: scale(.988);
}
.forum-card:has(.forum-card-link:active) { transform: scale(.995); }

.forum-card-icon {
  display: grid; place-items: center;
  width: 46px; height: 46px; flex-shrink: 0;
  border-radius: 14px;
  font-size: 18px;
  color: var(--ink-primary);
  background: color-mix(in oklab, var(--primary) 11%, transparent);
  transition: background .16s ease, transform .16s cubic-bezier(.2,.9,.3,1);
}
.forum-card-link:active .forum-card-icon {
  background: color-mix(in oklab, var(--primary) 20%, transparent);
  transform: scale(.94);
}

.forum-card-body { min-width: 0; flex: 1; }
.forum-card-title {
  font-size: 16px !important;
  font-weight: 650;
  line-height: 1.35;
  letter-spacing: -.01em;
}
.forum-card-meta {
  font-size: 13px !important;
  line-height: 1.45;
  color: var(--muted);
}


.forums-search { border-radius: 999px; }
.forums-hint { font-size: 12.5px; }

@media (max-width: 400px) {
  .forum-card-link { padding: 13px 13px; gap: 11px; }
  .forum-card-icon { width: 42px; height: 42px; font-size: 17px; }
  .forum-card-title { font-size: 15.5px !important; }
  .forum-card-meta { font-size: 12.5px !important; }
}

/* ============ 14. Chat room — messenger proportions ============ */

/* The room owns the viewport: header pinned, messages scroll, composer pinned.
   dvh (not vh) is what keeps BOTH the header and the input on screen while the
   on-screen keyboard is open — vh measures the pre-keyboard viewport. */
body.is-chat-page .chat-shell {
  /* The message context menu (#chatMsgMenu, styles.css) is a plain child of
     this element with position:absolute, and chats.js/dm.js compute its
     left/top directly from THIS element's getBoundingClientRect() — but
     without an explicit position here, .chat-shell is a normal static box,
     so the menu actually anchored to whatever positioned ancestor sat
     further up the tree instead, landing far from the message it was
     opened for. This makes .chat-shell the real positioning context the JS
     already assumed it was. */
  position: relative;
  display: flex;
  flex-direction: column;
  /* -12px is .main's own top+bottom padding on a chat page at this width
     (4px + 8px, styles.css) — .chat-shell sits inside .main, so that
     padding also eats into the space actually available below the header,
     not just --header-h itself. Missing it here pushed the composer a
     few px past the bottom of the viewport (mirrors the -28px desktop
     already subtracts below for its own margin-block). */
  /* --vv-height (router.js, visualViewport tracking) overrides 100dvh while
     the on-screen keyboard is open, on browsers where dvh doesn't shrink for
     it on its own — see the comment there for why that matters. */
  height: calc(var(--vv-height, 100dvh) - var(--header-h, 58px) - 12px);
  min-height: 0;
  overflow: hidden;
  /* Same warm tint as the login screen (body.is-auth-page, styles.css) —
     a flat white/black room read as either a blank sheet of paper or,
     in dark mode, total blackness behind the vivid red bubbles. */
  background:
    radial-gradient(900px 600px at 10% 10%, rgba(185, 28, 28, .06), transparent 60%),
    radial-gradient(800px 500px at 90% 90%, rgba(220, 38, 38, .04), transparent 60%),
    var(--bg);
}
/* Dark mode: a deliberately deep base with a faint warm glow (the same idea
   as light mode's dual radial-plus-base, just dialed way down so it reads as
   atmosphere, not a spotlight). This used to be a single flat #221716 that
   sat within a couple of luminance points of the "other" bubble's own
   dark-mode fill — room and bubble were nearly the same shade, so incoming
   messages had almost no visible surface of their own. Deepening the base
   here is what gives every layer above it (header, composer, bubbles) actual
   headroom to read as distinct, lighter surfaces. */
html[data-theme="dark"] body.is-chat-page .chat-shell {
  background:
    radial-gradient(900px 600px at 12% -10%, rgba(107, 28, 28, .12), transparent 60%),
    radial-gradient(700px 500px at 100% 100%, rgba(0, 0, 0, .5), transparent 60%),
    #1a1412;
}
body.is-chat-page .chat-head,
body.is-chat-page .chat-composer,
body.is-chat-page .chat-reply-bar { flex-shrink: 0; }

body.is-chat-page .chat-msgs {
  flex: 1;
  min-height: 0;
  overflow-y: auto;
  overscroll-behavior: contain;
  -webkit-overflow-scrolling: touch;
  scroll-padding-bottom: 8px;
}

/* Header reads as a conversation header, not a page title. */
.chat-head {
  display: flex; align-items: center; gap: 11px;
  padding: 9px 12px;
  border-bottom: 1px solid var(--border);
  background: color-mix(in oklab, var(--surface) 92%, transparent);
  backdrop-filter: saturate(1.6) blur(10px);
  position: sticky; top: 0; z-index: 3;
}
.chat-head-av { width: 38px; height: 38px; border-radius: 50%; flex-shrink: 0; }
.chat-head-info { min-width: 0; flex: 1; }
.chat-head-name { font-size: 15px; font-weight: 650; letter-spacing: -.01em; }
.chat-head-sub  { font-size: 11.5px; color: var(--muted); }

/* Composer: pill input + round send, sized for thumbs. */
.chat-composer {
  display: flex; align-items: flex-end; gap: 8px;
  padding: 9px 11px calc(9px + env(safe-area-inset-bottom));
  border-top: 1px solid var(--border);
  background: var(--surface);
}
.chat-composer textarea,
.chat-composer input[type="text"] {
  flex: 1; min-width: 0;
  min-height: 42px; max-height: 110px;
  padding: 11px 15px;
  border: 1px solid var(--border);
  border-radius: 21px;
  background: var(--bg);
  font-size: 16px;   /* under 16px is what actually triggers iOS Safari's zoom-on-focus, not over */
  line-height: 1.4;
  resize: none;
}
.chat-composer textarea:focus,
.chat-composer input[type="text"]:focus {
  border-color: var(--primary);
  box-shadow: 0 0 0 3px var(--ring);
  outline: none;
}
.chat-send-btn {
  width: 42px; height: 42px; flex-shrink: 0;
  display: grid; place-items: center;
  border-radius: 50%;
  transition: transform .12s, opacity .15s;
}
.chat-send-btn:active { transform: scale(.92); }

/* Desktop: hold the conversation to a phone-like column. A 1400px-wide
   message list is unreadable — the eye loses the thread between lines. */
@media (min-width: 900px) {
  body.is-chat-page .chat-shell {
    max-width: 760px;
    margin-inline: auto;
    border: 1px solid var(--border);
    border-radius: 20px;
    height: calc(100dvh - var(--header-h, 58px) - 28px);
    margin-block: 14px;
    box-shadow: 0 18px 50px -26px rgba(0,0,0,.28);
  }
  body.is-chat-page .chat-msgs { padding-inline: 18px; }
  .chat-composer { padding-bottom: 11px; }
}

/* The bottom tab bar has no business overlapping an open conversation. */
body.is-chat-page .mobile-tabs { display: none; }

/* Entry transition into a room */
.chat-enter { animation: chatEnter .26s cubic-bezier(.2,.86,.3,1) both; }
@keyframes chatEnter {
  from { opacity: 0; transform: translateY(8px) scale(.995); }
  to   { opacity: 1; transform: none; }
}

@media (prefers-reduced-motion: reduce) {
  .forum-card, .forum-card-link, .forum-card-icon { transition: none; }
  .forum-card-link:active { transform: none; }
  .chat-enter { animation: none; }
}

/* ============ 15. Chat subscriptions: empty state, add button, sheet ============ */

/* --- Empty panel --- */
.forums-onboard {
  display: flex; flex-direction: column; align-items: center;
  text-align: center;
  padding: 34px 18px 26px;
  animation: fOnboard .4s cubic-bezier(.2,.9,.28,1) both;
}
@keyframes fOnboard {
  from { opacity: 0; transform: translateY(10px); }
  to   { opacity: 1; transform: none; }
}
.forums-onboard-ico {
  display: grid; place-items: center;
  width: 64px; height: 64px; margin-bottom: 15px;
  border-radius: 20px;
  color: var(--ink-primary);
  background: color-mix(in oklab, var(--primary) 11%, transparent);
}
.forums-onboard-ico svg { width: 28px; height: 28px; }
.forums-onboard-title { margin: 0 0 7px; font-size: 18px; font-weight: 700; }
.forums-onboard-text {
  margin: 0 0 22px; max-width: 340px;
  font-size: 13.5px; line-height: 1.6; color: var(--muted);
}
/* Explicit (not just inherited from .btn-primary) — an <a> tag, so this
   guarantees white text regardless of what else touches link color. */
.forums-onboard a.btn-primary { color: #fff; }

/* --- Add button (doubles as the empty-state CTA) --- */
.forums-add {
  display: flex; align-items: center; gap: 13px;
  width: 100%; max-width: 420px;
  margin: 14px auto 0;
  padding: 14px 16px;
  border: 1.5px dashed color-mix(in oklab, var(--primary) 42%, var(--border));
  border-radius: 18px;
  background: color-mix(in oklab, var(--primary) 4%, var(--surface));
  color: var(--text);
  font: inherit; text-align: left; cursor: pointer;
  -webkit-tap-highlight-color: transparent;
  transition: border-color .18s, background .18s, transform .14s cubic-bezier(.2,.9,.3,1);
}
.forums-add:hover:not(:disabled) {
  border-color: var(--primary);
  background: color-mix(in oklab, var(--primary) 9%, var(--surface));
}
.forums-add:active:not(:disabled) { transform: scale(.985); }
.forums-add:disabled { opacity: .5; cursor: default; border-style: solid; }

/* On mobile, the add-subject CTA is pinned to the very bottom of the screen
   (above the tab bar) instead of sitting at the end of a scrollable list, so
   it stays reachable without hunting for it. chats.js renders it as a
   separate copy portaled onto <body> (syncChatsAddFab, #chatsAddFab) rather
   than inline for this breakpoint at all — #app plays a `transform`
   animation on every navigation, which gives position:fixed descendants of
   #app a containing block other than the viewport for the animation's
   duration, so an inline copy here would sit near the top of the content
   for ~420ms before jumping down to its real fixed position on every single
   visit to /chats. Desktop keeps the plain in-flow `.forums-add` above,
   where it's never fixed and none of this applies. */
@media (max-width: 768px) {
  #forumsList { padding-bottom: 82px; }
  .forums-onboard { padding-bottom: 82px; }
  #chatsAddFab .forums-add {
    display: flex;
    position: fixed;
    left: 16px; right: 16px; bottom: calc(76px + env(safe-area-inset-bottom));
    z-index: var(--z-chrome);
    max-width: none;
    margin: 0;
    padding: 11px 13px;
    gap: 10px;
    box-shadow: 0 10px 26px -8px rgba(0,0,0,.28);
  }
  #chatsAddFab .forums-add-ico { width: 34px; height: 34px; border-radius: 11px; }
  #chatsAddFab .forums-add-ico svg { width: 18px; height: 18px; }
  #chatsAddFab .forums-add-title { font-size: 13.5px; }
  #chatsAddFab .forums-add-sub { font-size: 11px; }
}

.forums-add-ico {
  display: grid; place-items: center;
  width: 42px; height: 42px; flex-shrink: 0;
  border-radius: 13px;
  color: #fff;
  background: linear-gradient(145deg, var(--primary), color-mix(in oklab, var(--primary) 76%, #000));
}
.forums-add-txt { display: flex; flex-direction: column; min-width: 0; gap: 2px; }
.forums-add-title { font-size: 15px; font-weight: 650; }
.forums-add-sub { font-size: 12px; color: var(--muted); }

/* --- Remove control on a subscribed card --- */
.forum-remove-btn {
  display: grid; place-items: center;
  width: 30px; flex-shrink: 0;
  border: 0; background: transparent;
  color: var(--muted);
  cursor: pointer;
  opacity: 0;
  transition: opacity .16s, color .16s;
}
.forum-card:hover .forum-remove-btn,
.forum-remove-btn:focus-visible { opacity: 1; }
.forum-remove-btn:hover { color: var(--danger); }
/* Touch devices have no hover, so keep it permanently visible but quiet. */
@media (hover: none) {
  .forum-remove-btn { opacity: .45; }
}

/* --- Add sheet --- */
.cadd-backdrop {
  position: fixed; inset: 0;
  z-index: 150;
  display: flex; align-items: flex-end; justify-content: center;
  background: color-mix(in oklab, var(--text) 34%, transparent);
  backdrop-filter: blur(3px);
  opacity: 0;
  transition: opacity .24s ease;
}
.cadd-backdrop.is-in { opacity: 1; }

.cadd-sheet {
  display: flex; flex-direction: column;
  width: 100%; max-width: 520px;
  max-height: min(84dvh, 660px);
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 22px 22px 0 0;
  box-shadow: 0 -12px 44px -14px rgba(0,0,0,.34);
  transform: translateY(24px);
  opacity: 0;
  transition: transform .3s cubic-bezier(.2,.9,.26,1), opacity .22s ease;
  overflow: hidden;
}
.cadd-backdrop.is-in .cadd-sheet { transform: none; opacity: 1; }

.cadd-grab {
  width: 38px; height: 4px; flex-shrink: 0;
  margin: 9px auto 3px;
  border-radius: 999px;
  background: var(--border-strong);
}
.cadd-head {
  display: flex; align-items: center; gap: 10px;
  padding: 6px 15px 11px;
  flex-shrink: 0;
}
.cadd-title { margin: 0; flex: 1; font-size: 16px; font-weight: 700; }
.cadd-x {
  display: grid; place-items: center;
  width: 31px; height: 31px; flex-shrink: 0;
  border: 1px solid transparent; border-radius: 9px;
  background: transparent; color: var(--muted); cursor: pointer;
  transition: background .15s, color .15s, border-color .15s;
}
.cadd-x:hover { background: var(--bg); color: var(--text); border-color: var(--border); }

.cadd-search {
  display: flex; align-items: center; gap: 8px;
  flex-shrink: 0;
  margin: 0 15px 10px;
  padding: 0 13px;
  border: 1px solid var(--border);
  border-radius: 999px;
  background: var(--bg);
  color: var(--muted);
  transition: border-color .16s, box-shadow .16s;
}
.cadd-search:focus-within {
  border-color: var(--primary);
  box-shadow: 0 0 0 3px var(--ring);
  color: var(--ink-primary);
}
.cadd-search input {
  flex: 1; min-width: 0;
  border: 0; outline: none; background: transparent;
  color: var(--text); font: inherit; font-size: 15px;
  padding: 10px 0;
}

.cadd-list {
  flex: 1; min-height: 0;
  overflow-y: auto;
  overscroll-behavior: contain;
  -webkit-overflow-scrolling: touch;
  padding: 0 11px 6px;
  display: flex; flex-direction: column; gap: 5px;
}

.cadd-row {
  display: flex; align-items: center; gap: 11px;
  padding: 11px 12px;
  border: 1px solid var(--border);
  border-radius: 14px;
  background: var(--surface);
  color: inherit; font: inherit;
  text-align: left; cursor: pointer;
  -webkit-tap-highlight-color: transparent;
  transition: border-color .15s, background .15s, transform .12s;
}
.cadd-row:active { transform: scale(.99); }
.cadd-row.is-on {
  border-color: var(--primary);
  background: color-mix(in oklab, var(--primary) 8%, var(--surface));
}

.cadd-check {
  display: grid; place-items: center;
  width: 23px; height: 23px; flex-shrink: 0;
  border: 1.6px solid var(--border-strong);
  border-radius: 7px;
  color: transparent;
  transition: background .15s, border-color .15s, color .15s, transform .15s;
}
.cadd-row.is-on .cadd-check {
  background: var(--primary-fill);
  border-color: var(--primary);
  color: #fff;
  transform: scale(1.05);
}

.cadd-body { display: flex; flex-direction: column; min-width: 0; gap: 2px; }
.cadd-name { font-size: 14.5px; font-weight: 600; line-height: 1.35; }
.cadd-meta { font-size: 12px; color: var(--muted); }

.cadd-empty { padding: 26px 12px; text-align: center; font-size: 13px; color: var(--muted); }

.cadd-foot {
  display: flex; align-items: center; gap: 9px;
  flex-shrink: 0;
  padding: 11px 15px calc(13px + env(safe-area-inset-bottom));
  border-top: 1px solid var(--border);
}
.cadd-foot .btn { border-radius: 999px; font-size: 13.5px; }
.cadd-all { flex-shrink: 0; }
.cadd-foot #caddConfirm { flex: 1; }

@media (min-width: 700px) {
  .cadd-backdrop { align-items: center; }
  .cadd-sheet { border-radius: 22px; max-height: min(78dvh, 640px); }
}

@media (prefers-reduced-motion: reduce) {
  .forums-onboard, .cadd-sheet, .cadd-backdrop { animation: none; transition: none; }
  .forums-add:active, .cadd-row:active { transform: none; }
}

/* ============ 16. Seminar chats as lecturer chips ============
   They used to live behind a collapsed accordion, so in practice nobody saw
   them. Now they sit in the subject card, always visible, one chip per
   lecturer — which is the only detail a student needs to find their group. */

.sem-chips {
  display: flex; flex-wrap: wrap; align-items: center; gap: 6px;
  padding: 0 14px 13px 73px;   /* aligns under the title, past the 46px icon */
}
@media (max-width: 400px) {
  .sem-chips { padding: 0 12px 11px 12px; }
}

.sem-chip {
  display: inline-flex; align-items: center; gap: 6px;
  max-width: 100%;
  padding: 6px 11px;
  border: 1px solid var(--border);
  border-radius: 11px;
  background: var(--bg);
  color: var(--text);
  font: inherit; font-size: 12.5px; font-weight: 600; line-height: 1.3;
  text-decoration: none; cursor: pointer;
  -webkit-tap-highlight-color: transparent;
  transition: border-color .15s, background .15s, transform .12s, color .15s;
}
.sem-chip:active { transform: scale(.96); }
.sem-chip > span { flex-shrink: 0; }

/* Joined — a live room the student can walk into */
a.sem-chip.is-joined {
  border-color: color-mix(in oklab, var(--primary) 38%, transparent);
  background: color-mix(in oklab, var(--primary) 10%, var(--surface));
  color: var(--ink-primary);
}
a.sem-chip.is-joined:hover {
  border-color: var(--primary);
  background: color-mix(in oklab, var(--primary) 16%, var(--surface));
}
.sem-chip-dot {
  width: 6px; height: 6px; border-radius: 50%;
  background: currentColor;
}

/* Not joined — tap to join */
.sem-chip.is-locked { color: var(--muted); border-style: dashed; }
.sem-chip.is-locked:hover {
  color: var(--ink-primary);
  border-color: color-mix(in oklab, var(--primary) 42%, transparent);
  border-style: solid;
  background: color-mix(in oklab, var(--primary) 6%, var(--surface));
}
.sem-chip-lock { display: inline-grid; place-items: center; opacity: .8; }
.sem-chip-lock svg { width: 12px; height: 12px; }

/* Admin chip: name links into the room, × removes the seminar */
.sem-chip.is-admin,
.sem-chip.is-split { padding: 0; gap: 0; overflow: hidden; }
.sem-chip-label {
  display: inline-flex; align-items: center; gap: 6px;
  padding: 6px 9px 6px 11px;
  color: inherit; text-decoration: none;
}
.sem-chip-label:hover { color: var(--ink-primary); }
.sem-chip-del {
  display: grid; place-items: center;
  width: 26px; align-self: stretch;
  border: 0; border-left: 1px solid var(--border);
  background: transparent; color: var(--muted);
  cursor: pointer;
  transition: background .15s, color .15s;
}
.sem-chip-del:hover { background: color-mix(in oklab, var(--danger) 12%, transparent); color: var(--danger); }
.sem-chip-del svg { width: 13px; height: 13px; }

/* Admin "+ სემინარი" */
.sem-chip-add {
  border-style: dashed;
  border-color: color-mix(in oklab, var(--primary) 40%, var(--border));
  color: var(--ink-primary);
  background: transparent;
}
.sem-chip-add:hover { background: color-mix(in oklab, var(--primary) 8%, transparent); border-style: solid; }

/* Admin add form drops onto its own line below the chips */
.seminar-add-box {
  display: flex; align-items: center; gap: 7px; flex-wrap: wrap;
  width: 100%;
  margin-top: 2px;
}
.seminar-add-box select {
  flex: 1; min-width: 160px;
  padding: 7px 10px;
  border: 1px solid var(--border); border-radius: 10px;
  background: var(--bg); color: var(--text);
  font: inherit; font-size: 12.5px;
}
.seminar-btn {
  padding: 7px 13px;
  border: 1px solid var(--border); border-radius: 10px;
  background: var(--surface); color: var(--text);
  font: inherit; font-size: 12.5px; font-weight: 600;
  cursor: pointer;
  transition: background .15s, border-color .15s;
}
.seminar-btn-join {
  background: var(--primary-fill); border-color: var(--primary); color: #fff;
}
.seminar-btn-join:hover { opacity: .9; }

/* The card keeps its bottom padding only when chips are present. */
.forum-card.has-seminars .forum-card-link { padding-bottom: 11px; }

@media (prefers-reduced-motion: reduce) {
  .sem-chip { transition: none; }
  .sem-chip:active { transform: none; }
}


/* ============================================================
   Pre-launch polish. Everything below is additive and lives here rather than
   in styles.css so it stays easy to review or revert (refinements.css loads
   later, so equal-specificity rules here win).
   ============================================================ */

/* ---- 1. Shadows that disappeared in dark mode ----
   These six were plain black rgba() with no dark-theme counterpart, so on a
   near-black page they rendered as nothing at all: the surface they were
   meant to lift had no edge left. Four can just use the theme tokens (which
   already have dark overrides); the two with bespoke geometry get a token of
   their own. --shadow-ink is new. */
:root { --shadow-ink: #100c0c; }
html[data-theme="dark"] { --shadow-ink: #000; }

.chat-layout .chat-sidebar { box-shadow: var(--shadow-md); }
.cb-list { box-shadow: var(--shadow-md); }
/* rgba(0,0,0,.03) - invisible in BOTH themes, i.e. a dead declaration. */
.faq-item { box-shadow: var(--shadow-xs); }
.auth-page .auth-tabs::before { box-shadow: var(--shadow-xs); }

@media (max-width: 768px) {
  .more-sheet { box-shadow: 0 -20px 60px color-mix(in oklab, var(--shadow-ink) 25%, transparent); }
  html[data-theme="dark"] .more-sheet { box-shadow: 0 -20px 60px color-mix(in oklab, var(--shadow-ink) 55%, transparent); }
}
.contact-admin-btn:hover { box-shadow: 0 6px 16px -6px color-mix(in oklab, var(--shadow-ink) 40%, transparent); }
html[data-theme="dark"] .contact-admin-btn:hover { box-shadow: 0 6px 16px -6px color-mix(in oklab, var(--shadow-ink) 70%, transparent); }

/* ---- 2. Touch targets below 44px ----
   styles.css already enforces 44x44 on buttons/inputs at this breakpoint, but
   these six were never in that list. The hit area is grown with a centred
   pseudo-element rather than min-width/min-height, because forcing real size
   on .chat-icon-btn would widen the chat header and push the title around -
   this changes zero pixels of layout. */
@media (max-width: 768px) {
  .cal-day-pop-close, .chat-reply-bar-x, .report-modal-x, .notif-del, .chat-icon-btn { position: relative; }
  .cal-day-pop-close::after, .chat-reply-bar-x::after, .report-modal-x::after,
  .notif-del::after, .chat-icon-btn::after {
    content: ""; position: absolute; top: 50%; left: 50%;
    transform: translate(-50%, -50%);
    width: max(100%, 44px); height: max(100%, 44px);
  }
  /* The exception: this one sits in a row of five emoji, so an overlapping
     44px halo would steal taps from its neighbours. Grown for real instead -
     it lives inside a popover, so nothing else shifts. */
  .chat-menu-react { padding: 11px 0; }
}

/* ---- 3. Keyboard focus ----
   styles.css's focus-visible list misses these nine, so keyboard users lose
   their place inside chat, notifications, the calendar and the FAQ. */
.chat-icon-btn:focus-visible, .notif-item-main:focus-visible, .notif-del:focus-visible,
.chat-menu-item:focus-visible, .chat-menu-react:focus-visible, .cal-cell:focus-visible,
.faq-q:focus-visible, .sem-chip:focus-visible, .wiz-kind-btn:focus-visible {
  outline: 3px solid var(--primary);
  outline-offset: 2px;
  box-shadow: 0 0 0 5px var(--ring);
}

/* ---- 4. Press feedback on the highest-frequency taps ----
   Cards already have it (styles.css); these did not. */
.notif-del:active, .notif-item-main:active, .cal-cell:active, .chat-icon-btn:active {
  transform: scale(.94);
}
@media (prefers-reduced-motion: reduce) {
  .notif-del:active, .notif-item-main:active, .cal-cell:active, .chat-icon-btn:active { transform: none; }
}

/* ---- 5. Utility classes for the inline styles that repeat most ----
   Defined here so NEW markup can use them; the ~90 existing inline copies are
   deliberately left alone. An inline style has specificity 1000 and always
   wins, so swapping them for classes wholesale would quietly change which rule
   applies in places that currently rely on the inline one - a real regression
   surface for no visible gain. */
.txt-xs { font-size: 12px; }
.icon-row { display: inline-flex; align-items: center; gap: 6px; }
.mt-14 { margin-top: 14px; }
.center-pad { text-align: center; padding: 32px; }
.btn-xs { padding: 4px 10px; font-size: 12px; }
.flex-fill { flex: 1; min-width: 0; }
.input-xs { width: 100%; font-size: 12px; padding: 6px 8px; }

/* The icon inside an empty state: styles.css sizes .empty .ico for an emoji
   glyph (56px font-size), which does nothing for an inline SVG. */
.empty .ico svg { width: 44px; height: 44px; opacity: .75; }

/* ---- 6. First-time feature hints (tour.js) ----
   Two shapes: a dismissible inline banner (pageNote) and a floating tooltip
   + pulsing ring anchored to a real element (mountSpotlight/startSequence).
   Deliberately no full-screen dimming — the rest of the page stays fully
   interactive while a hint is showing, so it teaches without blocking. */
:root { --z-coachmark: 150; } /* between --z-modal (101) and --z-toast (200) */

.hint-banner {
  padding: 14px 40px 14px 16px;
  border-left: 3px solid var(--primary);
  margin-bottom: 14px;
}
.hint-banner-row { display: flex; align-items: flex-start; gap: 12px; }
.hint-banner-ico { flex-shrink: 0; color: var(--primary); display: flex; align-items: center; }
.hint-banner-title { margin: 0 0 4px; font-size: 14px; }
.hint-banner-text { margin: 0; font-size: 13px; line-height: 1.55; color: var(--muted); }
.hint-banner-x {
  position: absolute; top: 10px; right: 10px;
  width: 28px; height: 28px; display: flex; align-items: center; justify-content: center;
  border: 0; background: transparent; border-radius: 50%; color: var(--muted); cursor: pointer;
  transition: background-color .15s ease, color .15s ease;
}
.hint-banner-x:hover { background: color-mix(in oklab, var(--primary) 10%, transparent); color: var(--ink-primary); }

.spotlight-ring {
  position: fixed; z-index: var(--z-coachmark);
  border-radius: 10px; pointer-events: none;
  box-shadow: 0 0 0 3px color-mix(in oklab, var(--primary) 55%, transparent),
              0 0 0 999px color-mix(in oklab, var(--shadow-ink) 12%, transparent);
  animation: spotlightPulse 1.8s ease-in-out infinite;
}
html[data-theme="dark"] .spotlight-ring {
  box-shadow: 0 0 0 3px color-mix(in oklab, var(--primary) 65%, transparent),
              0 0 0 999px color-mix(in oklab, var(--shadow-ink) 28%, transparent);
}
@keyframes spotlightPulse {
  0%, 100% { opacity: 1; }
  50% { opacity: .55; }
}

.spotlight-tip {
  position: fixed; z-index: var(--z-coachmark);
  max-width: min(300px, calc(100vw - 20px));
  background: var(--surface); border: 1px solid var(--border);
  border-radius: var(--radius); padding: 14px 16px;
  box-shadow: 0 14px 34px -10px color-mix(in oklab, var(--shadow-ink) 30%, transparent);
  animation: spotlightIn .22s ease both;
}
html[data-theme="dark"] .spotlight-tip {
  box-shadow: 0 14px 34px -10px color-mix(in oklab, var(--shadow-ink) 60%, transparent);
}
@keyframes spotlightIn { from { opacity: 0; transform: translateY(4px); } to { opacity: 1; transform: none; } }
.spotlight-arrow {
  position: absolute; left: var(--arrow-left, 20px); width: 12px; height: 12px;
  background: var(--surface); border: 1px solid var(--border);
  transform: translateX(-50%) rotate(45deg);
}
.spotlight-tip[data-placement="top"] .spotlight-arrow {
  bottom: -7px; border-top: 0; border-left: 0;
}
.spotlight-tip[data-placement="bottom"] .spotlight-arrow {
  top: -7px; border-bottom: 0; border-right: 0;
}
.spotlight-body h4 { margin: 0 0 4px; font-size: 13.5px; }
.spotlight-body p { margin: 0; font-size: 12.5px; line-height: 1.5; color: var(--muted); }
.spotlight-dots { display: flex; gap: 5px; margin: 10px 0 0; }
.spotlight-dots span { width: 6px; height: 6px; border-radius: 50%; background: var(--border); }
.spotlight-dots span.is-active { background: var(--primary); }
.spotlight-actions { display: flex; justify-content: flex-end; gap: 6px; margin-top: 12px; }

@media (prefers-reduced-motion: reduce) {
  .spotlight-ring { animation: none; }
  .spotlight-tip { animation: none; }
}

/* ---- 7. 3D bottom-nav buttons ----
   styles.css leaves .tab-btn flat and transparent by design ("No pill, no
   lift" per its own comment) - the icons stay exactly as drawn (flat line
   art, untouched here), but the button surface itself now reads as a raised
   physical key, with the active tab as a distinct, deeper-pressed red one. */
.mobile-tabs .tab-btn {
  background: linear-gradient(180deg,
    color-mix(in oklab, var(--surface) 100%, white 6%),
    var(--surface));
  border-radius: 14px;
  box-shadow:
    0 3px 0 color-mix(in oklab, var(--shadow-ink) 16%, transparent),
    0 5px 10px -4px color-mix(in oklab, var(--shadow-ink) 22%, transparent),
    inset 0 1px 0 color-mix(in oklab, white 55%, transparent);
  transform: translateY(0);
  transition: transform .12s ease, box-shadow .12s ease, background .18s ease, color .18s ease;
}
html[data-theme="dark"] .mobile-tabs .tab-btn {
  background: linear-gradient(180deg,
    color-mix(in oklab, var(--surface) 100%, white 4%),
    var(--surface));
  box-shadow:
    0 3px 0 color-mix(in oklab, var(--shadow-ink) 55%, transparent),
    0 6px 14px -4px color-mix(in oklab, var(--shadow-ink) 60%, transparent),
    inset 0 1px 0 color-mix(in oklab, white 10%, transparent);
}
/* Pressed: the raised key sinks in - shadow collapses to an inset dent and
   the button drops by exactly the height of its own resting shadow. */
.mobile-tabs .tab-btn:active {
  transform: translateY(3px);
  box-shadow:
    0 0 0 transparent,
    inset 0 2px 5px color-mix(in oklab, var(--shadow-ink) 30%, transparent);
}
html[data-theme="dark"] .mobile-tabs .tab-btn:active {
  box-shadow: inset 0 2px 6px color-mix(in oklab, var(--shadow-ink) 65%, transparent);
}
/* The selected tab is its own glossy red button, not just a color swap. */
.mobile-tabs .tab-btn.active {
  background: linear-gradient(180deg,
    color-mix(in oklab, var(--primary) 90%, white 10%),
    var(--primary));
  color: #fff;
  box-shadow:
    0 3px 0 color-mix(in oklab, var(--primary) 55%, black 30%),
    0 6px 14px -4px color-mix(in oklab, var(--primary) 55%, transparent),
    inset 0 1px 0 color-mix(in oklab, white 45%, transparent);
}
.mobile-tabs .tab-btn.active:active {
  transform: translateY(3px);
  box-shadow: inset 0 2px 6px color-mix(in oklab, black 35%, transparent);
}
/* The dark-mode default .tab-btn rule above carries an html[data-theme]
   type selector, which outranks a plain .active class selector - without
   this, dark mode silently fell back to the neutral keycap background for
   the selected tab too. */
html[data-theme="dark"] .mobile-tabs .tab-btn.active {
  background: linear-gradient(180deg,
    color-mix(in oklab, var(--primary) 90%, white 10%),
    var(--primary));
  box-shadow:
    0 3px 0 color-mix(in oklab, var(--primary) 55%, black 40%),
    0 6px 16px -4px color-mix(in oklab, var(--primary) 60%, transparent),
    inset 0 1px 0 color-mix(in oklab, white 35%, transparent);
}
html[data-theme="dark"] .mobile-tabs .tab-btn.active:active {
  box-shadow: inset 0 2px 6px color-mix(in oklab, black 45%, transparent);
}

@media (prefers-reduced-motion: reduce) {
  .mobile-tabs .tab-btn { transition: none; }
}

/* ---- 8. 3D dashboard tool tiles ("ხელსაწყოები") ----
   The four quick-access cards (Chats/Resources/FAQ/Ratings) were flat tinted
   cards with only a hover lift. Same raised-keycap language as the bottom
   nav buttons above: a gradient surface, a hard bottom edge for depth, and a
   real press-in dent on tap - each tile keeps its own per-destination tint
   (set by styles.css's .qi-forum/.qi-resources/.qi-faq/.qi-ratings), only the
   surface gains volume. */
.quick-grid-2 .quick {
  background: linear-gradient(180deg,
    color-mix(in oklab, var(--surface) 100%, white 5%),
    color-mix(in oklab, var(--primary) 8%, var(--surface)));
  box-shadow:
    0 3px 0 color-mix(in oklab, var(--primary) 20%, var(--shadow-ink) 10%),
    0 6px 14px -6px color-mix(in oklab, var(--shadow-ink) 20%, transparent),
    inset 0 1px 0 color-mix(in oklab, white 55%, transparent);
  transition: transform .12s ease, box-shadow .12s ease, border-color .18s ease;
}
html[data-theme="dark"] .quick-grid-2 .quick {
  background: linear-gradient(180deg,
    color-mix(in oklab, var(--surface) 100%, white 3%),
    color-mix(in oklab, var(--accent) 10%, var(--surface)));
  box-shadow:
    0 3px 0 color-mix(in oklab, var(--accent) 25%, var(--shadow-ink) 40%),
    0 6px 16px -6px color-mix(in oklab, var(--shadow-ink) 55%, transparent),
    inset 0 1px 0 color-mix(in oklab, white 8%, transparent);
}
.quick-grid-2 .quick:hover {
  transform: translateY(-3px);
  box-shadow:
    0 5px 0 color-mix(in oklab, var(--primary) 20%, var(--shadow-ink) 10%),
    0 10px 20px -8px color-mix(in oklab, var(--shadow-ink) 26%, transparent),
    inset 0 1px 0 color-mix(in oklab, white 55%, transparent);
}
html[data-theme="dark"] .quick-grid-2 .quick:hover {
  box-shadow:
    0 5px 0 color-mix(in oklab, var(--accent) 25%, var(--shadow-ink) 40%),
    0 12px 24px -8px color-mix(in oklab, var(--shadow-ink) 60%, transparent),
    inset 0 1px 0 color-mix(in oklab, white 8%, transparent);
}
/* Pressed: sinks by exactly its own resting shadow height, same as the
   bottom-nav treatment. */
.quick-grid-2 .quick:active {
  transform: translateY(3px);
  box-shadow: inset 0 2px 6px color-mix(in oklab, var(--shadow-ink) 22%, transparent);
}
html[data-theme="dark"] .quick-grid-2 .quick:active {
  box-shadow: inset 0 2px 6px color-mix(in oklab, var(--shadow-ink) 55%, transparent);
}

/* The icon badge becomes its own small raised button rather than a flat
   tinted disc - background/color stay whatever styles.css already set per
   destination (.qi-forum etc.), only depth is added. */
.quick-grid-2 .quick .qi {
  box-shadow:
    0 2px 0 color-mix(in oklab, black 12%, transparent),
    0 3px 6px -2px color-mix(in oklab, black 18%, transparent),
    inset 0 1px 0 color-mix(in oklab, white 45%, transparent);
}
html[data-theme="dark"] .quick-grid-2 .quick .qi {
  box-shadow:
    0 2px 0 color-mix(in oklab, black 35%, transparent),
    0 3px 8px -2px color-mix(in oklab, black 45%, transparent),
    inset 0 1px 0 color-mix(in oklab, white 12%, transparent);
}

@media (prefers-reduced-motion: reduce) {
  .quick-grid-2 .quick { transition: none; }
}

/* Desktop only — these 4 dashboard-tool icons render as circles (.card.quick
   .qi and .quick-grid-2 .quick .qi both set border-radius:50%/var(--radius-full)
   above); square them off with rounded corners instead. !important for the
   same reason as this file's own .card.quick override near the top — equal
   specificity, so the later declaration would otherwise need source-order
   luck instead of an explicit win. */
@media (min-width: 769px) {
  .card.quick .qi,
  .quick-grid-2 .quick .qi { border-radius: var(--radius-sm) !important; }
}

/* ---- 9. Soft depth, site-wide ----
   Sections 7 and 8 above gave the bottom nav and the four dashboard tiles a
   raised-keycap treatment, but everything else stayed flat at rest: buttons
   and cards only gained a shadow on :hover, which on a touch device means
   never. This extends that same language - hard bottom edge for the physical
   edge, soft ambient below it, inset top highlight for the lit surface - to
   the components the whole app is built from, at a deliberately lower
   intensity than sections 7-8 since these appear dozens of times per screen.

   Same theme rule as above: the tinted edge uses --primary in light mode and
   --accent in dark, because --primary is far darker in dark mode (#5d1818)
   and would read as a black edge. Everything neutral goes through
   --shadow-ink, so Professor Mode and Exam Mode - which re-scope the token
   palette wholesale - inherit sane values instead of a stray red glow. */

/* Buttons. The shadow rule covers every variant except .btn-ghost (a
   transparent button with a raised edge reads as a rendering bug); the
   gradient is held back from .btn-primary and .btn-success as well, since
   those two set their own background and --primary-fill is itself a gradient
   in dark mode. The inset highlight alone is enough to give them volume. */
.btn:not(.btn-ghost) {
  box-shadow:
    0 2px 0 color-mix(in oklab, var(--shadow-ink) 13%, transparent),
    0 4px 9px -4px color-mix(in oklab, var(--shadow-ink) 18%, transparent),
    inset 0 1px 0 color-mix(in oklab, white 45%, transparent);
  transition: transform .13s cubic-bezier(.2, .9, .3, 1), box-shadow .13s ease,
              background-color .18s ease, border-color .18s ease, color .18s ease;
}
.btn:not(.btn-ghost):not(.btn-primary):not(.btn-success) {
  background: linear-gradient(180deg,
    color-mix(in oklab, var(--surface) 100%, white 5%),
    var(--surface));
}
html[data-theme="dark"] .btn:not(.btn-ghost) {
  box-shadow:
    0 2px 0 color-mix(in oklab, var(--shadow-ink) 50%, transparent),
    0 5px 12px -4px color-mix(in oklab, var(--shadow-ink) 55%, transparent),
    inset 0 1px 0 color-mix(in oklab, white 8%, transparent);
}
html[data-theme="dark"] .btn:not(.btn-ghost):not(.btn-primary):not(.btn-success) {
  background: linear-gradient(180deg,
    color-mix(in oklab, var(--surface) 100%, white 3%),
    var(--surface));
}
/* The primary button keeps its own --primary-fill background and instead gets
   a red edge, so it still reads as the loudest control on the page. */
.btn.btn-primary {
  box-shadow:
    0 2px 0 color-mix(in oklab, var(--primary) 55%, black 25%),
    0 5px 12px -4px color-mix(in oklab, var(--primary) 45%, transparent),
    inset 0 1px 0 color-mix(in oklab, white 38%, transparent);
}
html[data-theme="dark"] .btn.btn-primary {
  box-shadow:
    0 2px 0 color-mix(in oklab, var(--accent) 45%, black 35%),
    0 5px 14px -4px color-mix(in oklab, var(--accent) 40%, transparent),
    inset 0 1px 0 color-mix(in oklab, white 25%, transparent);
}
/* Hover keeps styles.css's -1px lift and grows the edge to match, rather than
   replacing one idiom with another. Left ungated (no hover:hover query) to
   match how .btn:hover already behaves in styles.css. */
.btn:not(.btn-ghost):hover {
  transform: translateY(-1px);
  box-shadow:
    0 3px 0 color-mix(in oklab, var(--shadow-ink) 15%, transparent),
    0 8px 16px -6px color-mix(in oklab, var(--shadow-ink) 24%, transparent),
    inset 0 1px 0 color-mix(in oklab, white 45%, transparent);
}
html[data-theme="dark"] .btn:not(.btn-ghost):hover {
  box-shadow:
    0 3px 0 color-mix(in oklab, var(--shadow-ink) 55%, transparent),
    0 9px 20px -6px color-mix(in oklab, var(--shadow-ink) 60%, transparent),
    inset 0 1px 0 color-mix(in oklab, white 8%, transparent);
}
.btn.btn-primary:hover {
  transform: translateY(-1px);
  box-shadow:
    0 3px 0 color-mix(in oklab, var(--primary) 55%, black 25%),
    0 9px 20px -6px color-mix(in oklab, var(--primary) 50%, transparent),
    inset 0 1px 0 color-mix(in oklab, white 38%, transparent);
}
html[data-theme="dark"] .btn.btn-primary:hover {
  box-shadow:
    0 3px 0 color-mix(in oklab, var(--accent) 45%, black 35%),
    0 9px 22px -6px color-mix(in oklab, var(--accent) 45%, transparent),
    inset 0 1px 0 color-mix(in oklab, white 25%, transparent);
}
/* Pressed: sinks by exactly its resting shadow height, the same way sections
   7 and 8 do. This deliberately replaces styles.css's scale(.94) press for
   these buttons - two press idioms on one control cancel each other out, and
   the sink is the one the rest of this file already committed to. */
.btn:not(.btn-ghost):active {
  transform: translateY(2px);
  box-shadow: inset 0 2px 5px color-mix(in oklab, var(--shadow-ink) 22%, transparent);
}
html[data-theme="dark"] .btn:not(.btn-ghost):active {
  box-shadow: inset 0 2px 6px color-mix(in oklab, var(--shadow-ink) 55%, transparent);
}
.btn.btn-primary:active {
  transform: translateY(2px);
  box-shadow: inset 0 2px 6px color-mix(in oklab, black 30%, transparent);
}
/* .btn-ghost stays flat by design, but still needs to acknowledge a tap. */
.btn-ghost:active { transform: translateY(1px); }

/* Framed icon buttons (header, calendar arrows, toolbars). Smaller edge than
   .btn - these are 38px squares, so a 2px edge would dominate the glyph. */
.btn-icon {
  box-shadow:
    0 1px 0 color-mix(in oklab, var(--shadow-ink) 12%, transparent),
    0 3px 7px -3px color-mix(in oklab, var(--shadow-ink) 16%, transparent),
    inset 0 1px 0 color-mix(in oklab, white 40%, transparent);
}
html[data-theme="dark"] .btn-icon {
  box-shadow:
    0 1px 0 color-mix(in oklab, var(--shadow-ink) 45%, transparent),
    0 3px 9px -3px color-mix(in oklab, var(--shadow-ink) 50%, transparent),
    inset 0 1px 0 color-mix(in oklab, white 7%, transparent);
}
/* styles.css's hover here is a focus-style 4px ring, which reads as a
   selection state rather than a lift; keep the ring but add the edge back. */
.btn-icon:hover {
  box-shadow:
    0 0 0 4px var(--ring),
    0 2px 0 color-mix(in oklab, var(--shadow-ink) 14%, transparent),
    0 5px 11px -4px color-mix(in oklab, var(--shadow-ink) 20%, transparent);
}
.btn-icon:active {
  transform: translateY(1px);
  box-shadow: inset 0 2px 4px color-mix(in oklab, var(--shadow-ink) 20%, transparent);
}

/* Cards. The softest treatment in this section: a 1px edge, because .card is
   the most repeated surface in the app and a 3px keycap edge on every one of
   them turns a page into corrugated iron.

   Four exclusions, each deliberate:
   - .next-event-card owns the strongest shadow on the dashboard on purpose
     (styles.css calls it "the ONE primary focus on the page") and has its own
     four-layer stack; adding to it would flatten that hierarchy.
   - .forum-card sits under a styles.css section titled "Forums page (minimal
     2D)" - that page is intentionally flat.
   - .quick is already section 8 above.
   - .card-active gets its selection ring re-stated below instead, since a
     box-shadow here would otherwise overwrite it. */
.card:not(.next-event-card):not(.forum-card):not(.quick):not(.card-active) {
  box-shadow:
    0 1px 0 color-mix(in oklab, var(--shadow-ink) 9%, transparent),
    0 3px 8px -4px color-mix(in oklab, var(--shadow-ink) 14%, transparent),
    inset 0 1px 0 color-mix(in oklab, white 40%, transparent);
}
html[data-theme="dark"] .card:not(.next-event-card):not(.forum-card):not(.quick):not(.card-active) {
  box-shadow:
    0 1px 0 color-mix(in oklab, var(--shadow-ink) 40%, transparent),
    0 4px 10px -4px color-mix(in oklab, var(--shadow-ink) 45%, transparent),
    inset 0 1px 0 color-mix(in oklab, white 6%, transparent);
}
/* Keeps styles.css's -3px lift; only the shadow underneath it changes, so the
   card rises off a visible edge instead of out of nothing. */
.card:not(.next-event-card):not(.forum-card):not(.quick):not(.card-active):hover {
  box-shadow:
    0 3px 0 color-mix(in oklab, var(--shadow-ink) 11%, transparent),
    0 12px 26px -10px color-mix(in oklab, var(--shadow-ink) 20%, transparent),
    inset 0 1px 0 color-mix(in oklab, white 40%, transparent);
}
html[data-theme="dark"] .card:not(.next-event-card):not(.forum-card):not(.quick):not(.card-active):hover {
  box-shadow:
    0 3px 0 color-mix(in oklab, var(--shadow-ink) 50%, transparent),
    0 14px 30px -10px color-mix(in oklab, var(--shadow-ink) 60%, transparent),
    inset 0 1px 0 color-mix(in oklab, white 6%, transparent);
}
/* Selection ring first, then the resting depth - so picking a card doesn't
   visibly flatten it. */
.card.card-active:not(.next-event-card):not(.forum-card) {
  box-shadow:
    0 0 0 3px var(--ring),
    0 1px 0 color-mix(in oklab, var(--shadow-ink) 9%, transparent),
    0 3px 8px -4px color-mix(in oklab, var(--shadow-ink) 14%, transparent);
}

/* Inputs get the inverse treatment: recessed, not raised. A form of sunken
   fields next to raised buttons is what makes either read as 3D at all - if
   everything is raised, nothing is. Kept to a 1px inner shadow so the field
   still looks typable rather than disabled.

   No :focus rule here on purpose - styles.css's focus ring (and the
   aria-invalid one) are higher specificity and must keep winning outright. */
input:not([type="checkbox"]):not([type="radio"]):not([type="range"]),
textarea, select {
  box-shadow: inset 0 1px 2px color-mix(in oklab, var(--shadow-ink) 7%, transparent);
}
html[data-theme="dark"] input:not([type="checkbox"]):not([type="radio"]):not([type="range"]),
html[data-theme="dark"] textarea,
html[data-theme="dark"] select {
  box-shadow: inset 0 1px 3px color-mix(in oklab, var(--shadow-ink) 40%, transparent);
}

/* The mobile "More" sheet rows - the one list in the app that is really a
   grid of tap targets, so they earn the keycap rather than staying flat. */
.sheet-item {
  box-shadow:
    0 2px 0 color-mix(in oklab, var(--shadow-ink) 11%, transparent),
    0 4px 10px -5px color-mix(in oklab, var(--shadow-ink) 16%, transparent),
    inset 0 1px 0 color-mix(in oklab, white 45%, transparent);
  transition: transform .13s cubic-bezier(.2, .9, .3, 1), box-shadow .13s ease, background .18s ease;
}
html[data-theme="dark"] .sheet-item {
  box-shadow:
    0 2px 0 color-mix(in oklab, var(--shadow-ink) 50%, transparent),
    0 5px 12px -5px color-mix(in oklab, var(--shadow-ink) 55%, transparent),
    inset 0 1px 0 color-mix(in oklab, white 7%, transparent);
}
.sheet-item:active {
  transform: translateY(2px);
  box-shadow: inset 0 2px 5px color-mix(in oklab, var(--shadow-ink) 22%, transparent);
}
html[data-theme="dark"] .sheet-item:active {
  box-shadow: inset 0 2px 6px color-mix(in oklab, var(--shadow-ink) 55%, transparent);
}

/* Chips read as small physical tokens, so a 1px edge is enough. */
.chip {
  box-shadow:
    0 1px 0 color-mix(in oklab, var(--shadow-ink) 10%, transparent),
    inset 0 1px 0 color-mix(in oklab, white 40%, transparent);
}
html[data-theme="dark"] .chip {
  box-shadow:
    0 1px 0 color-mix(in oklab, var(--shadow-ink) 45%, transparent),
    inset 0 1px 0 color-mix(in oklab, white 6%, transparent);
}
.chip:active { transform: translateY(1px); box-shadow: none; }

/* styles.css's global reduced-motion block already zeroes transition
   durations, but the transform end states still apply instantly - a button
   that teleports 2px down on every tap is exactly the kind of motion this
   query exists to remove. Mirrors the opt-outs in sections 7 and 8. */
@media (prefers-reduced-motion: reduce) {
  .btn:not(.btn-ghost), .btn-icon, .sheet-item { transition: none; }
  .btn:not(.btn-ghost):hover, .btn.btn-primary:hover { transform: none; }
  .btn:not(.btn-ghost):active, .btn.btn-primary:active, .btn-ghost:active,
  .btn-icon:active, .sheet-item:active, .chip:active { transform: none; }
}

/* ---- 10. Dashboard leaderboard rail (desktop two-column) ----
   The dashboard becomes `content | rail` on desktop. Gated on
   min-width:769px, which is the only breakpoint that fires in BOTH real
   desktop and the forced-desktop view mode (state.js rewrites the viewport to
   width=1100, so anything gated at 1200px+ would never appear there, and
   html[data-view="desktop"] would never match on an actual desktop). */
.dash-rail { display: none; }

@media (min-width: 769px) {
  .dash-shell {
    display: grid;
    /* minmax(0, 1fr), not 1fr: .dash-hero .dash-side carries a fixed
       width: calc(320px + 2cm + 12px + 80px) (~488px), which gives the left
       column a large min-content floor. A bare 1fr honours that floor and
       overflows the grid instead of shrinking. */
    grid-template-columns: minmax(0, 1fr) 300px;
    gap: 24px;
    align-items: start;
  }
  .dash-rail {
    display: block;
    position: sticky;
    /* Clears the fixed glass header; the rail is taller than the viewport
       once a page of 10 is rendered, so it scrolls with the content until it
       reaches the top and then holds. */
    top: 78px;
  }
  /* The hero's side card was sized for a full-width shell
     (width: calc(320px + 2cm + 12px + 80px) ~= 488px); at a ~780px column
     that leaves the greeting ~190px and wraps it onto three lines.
     Deliberately a flat px value and NOT a percentage or clamp with one:
     .dash-hero is `grid-template-columns: 1fr auto`, so this element lives in
     an auto-sized track with an indefinite width, against which a percentage
     resolves to nothing and silently collapses to the minimum.

     The +1cm grows the card leftward rather than rightward: the auto track is
     the right-hand one, so extra width moves its left edge and takes space
     from the greeting column, which has it to spare. (cm is the unit the
     original hero rule already used here.) */
     The trailing + 96px is the streak card (84px) plus the flex gap (12px).
     It was absent while this slot held the bare buildings photo, which simply
     overflowed the declared width; the streak card is a real card with a
     border and a background, so an overflow here is visible. */
  .dash-hero .dash-side { width: calc(320px + 1cm + 96px); }
}

/* Below 1240px the shell has no slack left for a 300px rail plus a hero that
   still wants ~490px, so the rail narrows before the content does. */
@media (min-width: 769px) and (max-width: 1100px) {
  .dash-shell { grid-template-columns: minmax(0, 1fr) 260px; gap: 18px; }
  /* No fixed width in this band, unlike every other rule for this element:
     there is no width that fits. Measured at a 790px viewport the hero has
     403px of content box, the greeting will not compress below ~154px and the
     gap takes 14 — leaving ~235px, less than the event card alone was being
     told to be. A fixed width here does not shrink, it overflows (the old
     calc(280px + 1cm) already spilled ~84px; it simply wasn't visible while
     the thing hanging off the edge was a transparent masked PNG rather than a
     bordered card).

     So the side is sized by its contents instead: the streak card keeps its
     fixed 84px and the event card becomes the flexible one — it already
     carries min-width: 0, which is what lets it shrink below its flex-basis
     rather than forcing the track wider. */
  .dash-hero .dash-side { width: auto; max-width: 100%; }
  .dash-hero .dash-side > .card { flex: 1 1 0; }
}

/* The dashboard tool tile that replaced "ჩატები". styles.css tints the other
   four (blue / --success / --warning / --accent); violet is the one distinct
   hue left — gold would have collided with the amber FAQ tile. */
.quick-grid-2 .quick .qi.qi-leaderboard {
  background: color-mix(in oklab, #8b5cf6 20%, transparent);
  color: #5b21b6;
}
html[data-theme="dark"] .quick-grid-2 .quick .qi.qi-leaderboard { color: #c4b5fd; }

.lb-card { padding: 14px; }
.lb-head { display: flex; align-items: center; justify-content: space-between; gap: 8px; }
.lb-title { margin: 0; font-size: 15px; }
.lb-scope { display: flex; gap: 6px; margin: 10px 0 12px; }
.lb-scope .btn { flex: 1 1 0; justify-content: center; padding-inline: 6px; }

/* styles.css sizes .lb-list for the admin modal (max-height 60vh + its own
   scroller). In a sticky rail that produces a scroll area inside a scroll
   area; the rail is already short enough to show all ten. */
.lb-card .lb-list { max-height: none; overflow: visible; gap: 4px; }
/* .lb-row is a <div> in the admin leaderboard and an <a> here. */
.lb-card .lb-row {
  text-decoration: none; color: inherit;
  transition: background-color .16s ease, transform .13s cubic-bezier(.2, .9, .3, 1);
}
.lb-card .lb-row:hover { background: color-mix(in oklab, var(--primary) 8%, var(--surface-2)); }
.lb-card .lb-row:active { transform: translateY(1px); }
.lb-card .lb-rank { flex-basis: 26px; font-size: 12.5px; }
/* Name over წოდება. The rail shows the tier, never the score — xp is a
   profile-only number. */
.lb-main { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 1px; }
.lb-card .lb-name { font-size: 13px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.lb-tier { font-size: 11px; color: var(--muted); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.lb-card .lb-status { font-size: 14px; flex-shrink: 0; }

/* Podium: the top three rows get progressively redder, #1 the strongest.
   Tinted through --primary (via color-mix) rather than hardcoded reds so
   Professor Mode and Exam Mode, which re-scope the palette wholesale, tint
   along with everything else instead of keeping a stray red. */
.lb-card .lb-row--top3 { background: color-mix(in oklab, var(--primary) 22%, var(--surface-2)); }
.lb-card .lb-row--top2 { background: color-mix(in oklab, var(--primary) 55%, var(--surface-2)); }
/* #1 is the brand red outright, not a tint of it — the top of the podium. */
.lb-card .lb-row--top1 { background: var(--primary); }
/* Both of the saturated fills need light text; the muted tier grey vanishes. */
.lb-card .lb-row--top1 .lb-name,
.lb-card .lb-row--top1 .lb-rank,
.lb-card .lb-row--top2 .lb-name,
.lb-card .lb-row--top2 .lb-rank { color: #fff; }
.lb-card .lb-row--top1 .lb-tier,
.lb-card .lb-row--top2 .lb-tier { color: color-mix(in oklab, #fff 80%, transparent); }
.lb-card .lb-row--top3 .lb-tier { color: color-mix(in oklab, var(--text) 70%, transparent); }

.lb-card .lb-row--top3:hover { background: color-mix(in oklab, var(--primary) 30%, var(--surface-2)); }
.lb-card .lb-row--top2:hover { background: color-mix(in oklab, var(--primary) 64%, var(--surface-2)); }
.lb-card .lb-row--top1:hover { background: color-mix(in oklab, var(--primary) 88%, black 12%); }

/* --primary is much darker in dark mode (#5d1818), so the same percentages
   would read as mud rather than red; --accent is the vivid one there, the same
   swap sections 7 and 8 make. */
html[data-theme="dark"] .lb-card .lb-row--top3 { background: color-mix(in oklab, var(--accent) 22%, var(--surface-2)); }
html[data-theme="dark"] .lb-card .lb-row--top2 { background: color-mix(in oklab, var(--accent) 55%, var(--surface-2)); }
html[data-theme="dark"] .lb-card .lb-row--top1 { background: var(--accent); }
/* #1's fill is --accent itself, a light red in dark mode (#f07a7a), so it is
   the one row that needs dark text. #2 sits at 55% accent over surface-2 —
   about rgb(144,84,83), a mid tone where near-black text reads as muddy and
   low-contrast, so it keeps white like its light-mode counterpart. */
html[data-theme="dark"] .lb-card .lb-row--top1 .lb-name,
html[data-theme="dark"] .lb-card .lb-row--top1 .lb-rank { color: #16090a; }
html[data-theme="dark"] .lb-card .lb-row--top1 .lb-tier { color: color-mix(in oklab, #16090a 75%, transparent); }
html[data-theme="dark"] .lb-card .lb-row--top2 .lb-name,
html[data-theme="dark"] .lb-card .lb-row--top2 .lb-rank { color: #fff; }
html[data-theme="dark"] .lb-card .lb-row--top2 .lb-tier { color: color-mix(in oklab, #fff 80%, transparent); }
html[data-theme="dark"] .lb-card .lb-row--top3 .lb-tier { color: color-mix(in oklab, var(--text) 74%, transparent); }
html[data-theme="dark"] .lb-card .lb-row--top3:hover { background: color-mix(in oklab, var(--accent) 30%, var(--surface-2)); }
html[data-theme="dark"] .lb-card .lb-row--top2:hover { background: color-mix(in oklab, var(--accent) 64%, var(--surface-2)); }
html[data-theme="dark"] .lb-card .lb-row--top1:hover { background: color-mix(in oklab, var(--accent) 88%, white 12%); }

/* Admin's university/faculty pickers, in place of the student scope toggle. */
.lb-admin-scope { display: grid; gap: 8px; margin: 10px 0 12px; }
.lb-admin-field { display: flex; flex-direction: column; gap: 3px; margin: 0; }
.lb-admin-field > span { font-size: 11px; color: var(--muted); font-weight: 600; }
.lb-admin-field select { padding: 7px 10px; font-size: 12.5px; }
.lb-page .lb-admin-scope { grid-template-columns: 1fr 1fr; gap: 12px; margin-bottom: 14px; }
@media (max-width: 560px) {
  .lb-page .lb-admin-scope { grid-template-columns: 1fr; }
}

/* The signed-in student's own row, so they can find themselves in the page.
   Every fill rule here is scoped :not(.lb-row--top) — a top-three row that is
   also yours must keep its podium colour, or landing on the podium would
   ironically flatten your row back to a pale tint and lose the ranking cue.
   (The dark-mode and :hover variants need the same guard: the first carries an
   html type selector and the second a pseudo-class, so either one would
   otherwise outrank the podium rules above.) */
.lb-row--me:not(.lb-row--top) {
  background: color-mix(in oklab, var(--primary) 14%, var(--surface-2));
  box-shadow: inset 0 0 0 1px color-mix(in oklab, var(--primary) 38%, transparent);
}
html[data-theme="dark"] .lb-row--me:not(.lb-row--top) {
  background: color-mix(in oklab, var(--accent) 16%, var(--surface-2));
  box-shadow: inset 0 0 0 1px color-mix(in oklab, var(--accent) 40%, transparent);
}
.lb-card .lb-row--me:not(.lb-row--top):hover {
  background: color-mix(in oklab, var(--primary) 20%, var(--surface-2));
}
html[data-theme="dark"] .lb-card .lb-row--me:not(.lb-row--top):hover {
  background: color-mix(in oklab, var(--accent) 24%, var(--surface-2));
}
/* On the podium the tint is already spoken for, so "this is you" becomes an
   inset ring in the fill's own contrast colour instead. */
.lb-card .lb-row--me.lb-row--top { box-shadow: inset 0 0 0 2px color-mix(in oklab, #fff 72%, transparent); }
.lb-card .lb-row--me.lb-row--top3 { box-shadow: inset 0 0 0 2px color-mix(in oklab, var(--primary) 55%, transparent); }
html[data-theme="dark"] .lb-card .lb-row--me.lb-row--top { box-shadow: inset 0 0 0 2px color-mix(in oklab, #16090a 60%, transparent); }
html[data-theme="dark"] .lb-card .lb-row--me.lb-row--top3 { box-shadow: inset 0 0 0 2px color-mix(in oklab, var(--accent) 60%, transparent); }

/* Standalone page (#/leaderboard) — same card, but the rail's 300px-tuned
   compaction is wasted on a full-width column, so the rows breathe a little.
   The scope buttons stop stretching edge to edge for the same reason. */
.lb-page { padding: 16px; }
.lb-page .lb-scope { gap: 8px; margin-bottom: 14px; }
.lb-page .lb-scope .btn { flex: 0 1 auto; padding-inline: 14px; }
.lb-page .lb-list { gap: 6px; }
.lb-page .lb-row { padding: 11px 12px; }
.lb-page .lb-rank { flex-basis: 40px; font-size: 13.5px; }
.lb-page .lb-name { font-size: 14.5px; }
.lb-page .lb-tier { font-size: 12px; }
.lb-page .lb-status { font-size: 17px; }

.lb-pager { display: flex; flex-wrap: wrap; gap: 4px; justify-content: center; margin-top: 12px; }
.lb-pager-btn { min-width: 28px; justify-content: center; padding-inline: 6px; font-variant-numeric: tabular-nums; }
.lb-pager-gap { color: var(--muted); font-size: 12px; align-self: center; padding: 0 2px; }

/* Exam mode strips the dashboard down to what a student needs mid-exam
   (styles.css hides .dash-side, .news-mini, .chip-row); a rankings board is
   exactly the kind of distraction that list exists for. */
html[data-exam-mode="1"] .dash-rail { display: none !important; }
html[data-exam-mode="1"] .dash-shell { grid-template-columns: minmax(0, 1fr); }

/* ---- 11b. Today's-events carousel: swipe had to work both ways ----
   styles.css sets `scroll-behavior: smooth` on .ne-scroll together with
   `scroll-snap-type: x mandatory`. That combination makes every scroll change
   — including the browser's own snap correction — an animation, and a
   backwards flick would get overridden by the in-flight animation and land
   back on the slide it started from: forward paged, backward did not.

   Nothing needed it: the dot buttons pass `behavior: "smooth"` to scrollTo
   themselves, and app.js's carousel init sets scrollLeft directly under the
   comment "Jump to initial slide without smooth animation" — an intent this
   declaration silently reversed. */
.next-event-card .ne-scroll { scroll-behavior: auto; }

/* ---- 11. Roomier dashboard tool tiles on mobile ----
   The four "ხელსაწყოები" tiles were sized for the desktop grid and read as
   cramped tap targets on a phone. Nudged up rather than redesigned: same 2x2
   grid, same centred column layout from styles.css's own mobile block.
   !important because this file's .card.quick rules above already use it (see
   the note at the end of section 8) and padding/border-radius there would
   otherwise win on source order alone. */
@media (max-width: 768px) {
  .quick-grid-2 .card.quick { padding: 13px 10px !important; }
  /* The icon is a flex item of the h3, and styles.css pins it with
     `flex: 0 0 18px` (see its own specificity note there). flex-basis wins
     over width on the main axis, so setting width alone stretched it into an
     18x22 oval — the flex shorthand has to be restated as well. */
  .quick-grid-2 .card.quick h3 .qi {
    width: 22px; height: 22px; flex: 0 0 22px;
  }
  .quick-grid-2 .card.quick h3 .qi svg { width: 13px; height: 13px; }
  .quick-grid-2 .card.quick h3 { font-size: 12.5px; }
  .quick-grid { gap: 10px; }
}

/* ---- 10. Daily streak card (dashboard hero, beside the event card) ----

   A narrow companion panel to .next-event-card: same top edge (margin-top
   matches it), same height (stretch takes the flex line's cross size, which
   the taller event card sets) and the same shadow stack, so the two read as
   one pair rather than a card and a sticker.

   It used to be pinned into the hero's bottom-right corner with negative
   margins, sized by a 424/522 aspect ratio, and carried the campus-buildings
   artwork as its own ::before. That artwork is now a layer on .dash-hero
   itself, drawn far larger, and this card sits on top of it (z-index: 1) with
   an opaque background — so none of it shows through the card. */
.streak-card {
  flex: 0 0 auto;
  width: 84px;
  align-self: stretch;
  margin-top: 14px;
  position: relative;
  z-index: 1;
  overflow: hidden;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 1px;
  padding: 6px 3px;
  border: 1px solid color-mix(in oklab, var(--primary) 26%, transparent);
  border-radius: var(--radius-lg);
  background: linear-gradient(160deg,
    color-mix(in oklab, var(--primary) 16%, var(--surface)),
    var(--surface));
  color: var(--text);
  cursor: pointer;
  font: inherit;
  text-align: center;
  transition: transform .16s ease, box-shadow .16s ease;
  /* The same four-layer stack .next-event-card uses, toned down on the two
     brand-coloured layers: that card's surface is a saturated gradient and
     this one is light, where full strength reads as a halo. */
  box-shadow:
    inset 0 1px 0 rgba(255, 255, 255, .18),
    0 10px 22px -10px color-mix(in oklab, var(--primary) 42%, transparent),
    0 30px 54px -18px color-mix(in oklab, var(--primary) 26%, transparent),
    0 10px 24px -8px rgba(0, 0, 0, .16);
}
html[data-theme="dark"] .streak-card {
  box-shadow:
    inset 0 1px 0 rgba(255, 255, 255, .06),
    0 10px 22px -10px rgba(0, 0, 0, .55),
    0 30px 54px -18px rgba(0, 0, 0, .45);
}
.streak-card:hover { transform: translateY(-2px); }
.streak-card:disabled { opacity: .6; cursor: progress; }

.streak-card > * { position: relative; z-index: 1; }
/* The flame is an inline SVG stroked with currentColor, so "lit" and "cold"
   are a colour change on this one element — no second icon, no filter.
   A hotter red than --primary (#c22929, the brand maroon): this is the one
   element on the card that should read as fire, and the brand red left it
   looking like just another icon. The translucent fill is what actually sells
   it — a bare 1.75px outline is too thin to carry a colour at this size. */
.streak-flame { display: inline-flex; color: #e11d1d; }
.streak-flame svg { fill: color-mix(in oklab, currentColor 20%, transparent); }
/* Sized here rather than at the call site so the mobile override below is
   the only place the number changes. */
.streak-card .streak-flame svg { width: 23px; height: 23px; }
html[data-theme="dark"] .streak-flame { color: #ff5f5f; }
html[data-theme="dark"] .streak-flame svg { fill: color-mix(in oklab, currentColor 26%, transparent); }
.streak-num {
  font-size: 19px; font-weight: 700; line-height: 1.1;
  font-variant-numeric: tabular-nums;
  color: var(--primary);
}
html[data-theme="dark"] .streak-num { color: var(--accent); }
.streak-lbl { font-size: 9.5px; color: var(--muted); letter-spacing: .02em; }

/* Nothing earned yet, or the streak has already lapsed — drained of colour so
   a stale number can never read as a live one. */
.streak-card.is-cold {
  border-color: var(--border);
  background: var(--surface);
}
.streak-card.is-cold .streak-num { color: var(--muted); }
.streak-card.is-cold .streak-flame,
html[data-theme="dark"] .streak-card.is-cold .streak-flame { color: var(--muted); opacity: .7; }
/* No fill when unlit — an outline reads as "not burning" far better than a
   grey blob, and it keeps the lit/cold difference obvious at a glance. */
.streak-card.is-cold .streak-flame svg,
html[data-theme="dark"] .streak-card.is-cold .streak-flame svg { fill: transparent; }

/* A rescuable streak. The badge is the only affordance telling the student the
   card is worth tapping today, so it gets the attention-pulling treatment. */
.streak-card.has-freeze { border-color: #38bdf8; }
.streak-freeze {
  position: absolute;
  top: 4px; right: 4px;
  font-size: 13px;
  line-height: 1;
  animation: streakFreezePulse 1.8s ease-in-out infinite;
}
@keyframes streakFreezePulse {
  0%, 100% { transform: scale(1); opacity: .85; }
  50%      { transform: scale(1.18); opacity: 1; }
}
@media (prefers-reduced-motion: reduce) {
  .streak-freeze { animation: none; }
  .streak-card:hover { transform: none; }
}

@media (max-width: 768px) {
  .streak-card { width: 72px; padding: 5px 2px; }
  .streak-card .streak-flame svg { width: 20px; height: 20px; }
  .streak-num { font-size: 18px; }
  .streak-lbl { font-size: 9px; }
}

/* ---- Profile: streak + achievement badges (inside "აქტივობის სტატუსი") ---- */
.streak-row {
  display: flex; align-items: center; gap: 8px;
  margin: 12px 0 10px; padding-top: 12px;
  border-top: 1px solid var(--border);
}
.streak-row.is-cold .streak-flame,
html[data-theme="dark"] .streak-row.is-cold .streak-flame { color: var(--muted); opacity: .7; }
.streak-row .streak-row-num { font-size: 17px; font-weight: 700; font-variant-numeric: tabular-nums; }
.ach-list { display: flex; gap: 6px; flex-wrap: wrap; }
.ach {
  display: inline-flex; flex-direction: column; align-items: center; gap: 1px;
  min-width: 52px; padding: 7px 6px;
  border: 1px solid var(--border); border-radius: var(--radius-sm);
  background: var(--bg-elev);
  font-size: 11px; color: var(--muted);
}
.ach .ach-d { font-size: 13px; font-weight: 700; color: var(--text); }
.ach.earned {
  border-color: color-mix(in oklab, var(--primary) 40%, transparent);
  background: color-mix(in oklab, var(--primary) 12%, var(--surface));
}
.ach.earned .ach-d { color: var(--primary); }
html[data-theme="dark"] .ach.earned .ach-d { color: var(--accent); }
.ach:not(.earned) { opacity: .55; }

/* ---- 11. DM inbox ----

   The rows are <a> elements and long-press deletes a conversation, so the
   platform's own long-press behaviours have to get out of the way: iOS shows a
   link callout/preview sheet and both platforms start a text selection, either
   of which fires over the top of the confirm dialog. */
.dm-row {
  -webkit-touch-callout: none;
  -webkit-user-select: none;
  user-select: none;
}
.dm-hint {
  font-size: 12px;
  margin: 12px 2px 0;
  text-align: center;
}
/* No pointer means no long-press; the desktop affordance is right-click, and
   the open conversation has a delete button of its own either way. */
@media (hover: hover) and (pointer: fine) {
  .dm-hint { display: none; }
}

/* ---- 12. Phone header slogan ----
   Sits in the space the desktop nav vacates below 769px (styles.css hides
   .nav there). Hidden on desktop, where that space is the navigation. */
.hdr-slogan {
  display: none;
  font-weight: 600;
  line-height: 1.2;
  white-space: nowrap;
  letter-spacing: -.01em;
  color: color-mix(in oklab, var(--primary) 78%, var(--text));
}
html[data-theme="dark"] .hdr-slogan { color: var(--accent); }

/* Desktop: centred in the gap between the last nav button and the header
   icons. That requires handing the free space to the slogan instead of the
   nav — .nav is flex: 1 1 auto by default and would otherwise absorb all of
   it, leaving the slogan pinned against the icons rather than between them.
   Only the nav's grow is given up; it can still shrink and scroll as before.

   A little larger than .nav a (15px vs 13.5px) so it reads as a statement
   rather than a fifth nav item sitting in the same row.

   Gated at 1024px rather than the usual 769px because between those two the
   header is genuinely full — logo, four nav boxes and the action icons already
   use the width, and .nav is the element that would start scrolling to make
   room. The slogan is decoration; the navigation is not. */
@media (min-width: 1024px) {
  .nav { flex: 0 1 auto; }
  .hdr-slogan {
    display: block;
    flex: 1 1 auto;
    min-width: 0;
    text-align: center;
    font-size: 15px;
    overflow: hidden;
    text-overflow: ellipsis;
  }
}

@media (max-width: 768px) {
  .hdr-slogan {
    display: block;
    flex: 1 1 auto;
    min-width: 0;
    text-align: center;
    /* Shrinks before it ever wraps or pushes the icons off-screen; the header
       is a fixed-height row and this is the only elastic thing in it. */
    font-size: clamp(13px, 4.7vw, 18px);
    overflow: hidden;
    text-overflow: ellipsis;
  }
}
/* Below ~360px there is genuinely no room for it beside the logo and two
   icons — dropping it beats truncating it to two words. */
@media (max-width: 359px) { .hdr-slogan { display: none; } }

/* ---- 13. Review cards expand on tap ----
   The "დეტალურად" button that used to sit under every review is gone; the
   card is the control now. It needs to look like one, and it needs to not
   fight the report button living inside it. */
.review-toggle { cursor: pointer; -webkit-tap-highlight-color: transparent; }
.review-toggle:hover { border-color: color-mix(in oklab, var(--primary) 30%, var(--border)); }
.review-toggle button, .review-toggle a { cursor: pointer; }
/* A quiet, always-visible hint that there is more underneath — replaces the
   button's job of announcing the breakdown exists. */
.review-toggle .crit-breakdown { margin-top: 6px; }
.review-toggle::after {
  content: "";
  display: block;
  width: 18px; height: 2px;
  margin: 6px auto 0;
  border-radius: 2px;
  background: color-mix(in oklab, var(--primary) 35%, transparent);
  transition: width .16s ease, background-color .16s ease;
}
.review-toggle.is-open::after {
  width: 34px;
  background: color-mix(in oklab, var(--primary) 60%, transparent);
}

/* ---- 14. Review rules ---- */
.rules-btn {
  width: 34px; height: 34px; padding: 0;
  display: inline-flex; align-items: center; justify-content: center;
  font-size: 16px; font-weight: 700; line-height: 1;
  border: 1px solid color-mix(in oklab, var(--primary) 30%, var(--border));
  border-radius: var(--radius-full, 999px);
  color: var(--ink-primary);
}
.rules-btn:hover { background: color-mix(in oklab, var(--primary) 10%, transparent); }
.review-rules { max-width: 520px; text-align: left; }
.review-rules-list {
  margin: 10px 0 0; padding-left: 20px;
  font-size: 13.5px; line-height: 1.65;
  display: flex; flex-direction: column; gap: 8px;
  /* Eight rules do not fit a phone screen; the modal scrolls rather than
     pushing its own "გასაგებია" button off the bottom. */
  max-height: 52vh; overflow-y: auto;
}
.review-rules-list b { color: var(--text); }

/* ---- 15. Long-press acknowledgement ----
   Fires the moment a long-press is RECOGNISED, before the menu/dialog it
   opens appears — the gap between the two is exactly where the gesture used
   to feel like nothing had happened. Deliberately brief: this is a receipt,
   not an animation. */
@keyframes lpFired {
  0%   { transform: scale(1);    box-shadow: 0 0 0 0 color-mix(in oklab, var(--primary) 45%, transparent); }
  35%  { transform: scale(.975); box-shadow: 0 0 0 6px color-mix(in oklab, var(--primary) 18%, transparent); }
  100% { transform: scale(1);    box-shadow: 0 0 0 12px transparent; }
}
.lp-fired {
  animation: lpFired .3s ease-out;
  /* The ring is drawn outside the element's own box; without this a card in a
     clipped list shows only part of it. */
  position: relative;
  z-index: 2;
}
@media (prefers-reduced-motion: reduce) {
  /* Still acknowledged, just without the movement. */
  .lp-fired { animation: none; outline: 2px solid color-mix(in oklab, var(--primary) 55%, transparent); outline-offset: 2px; }
}
