/* ---------- hub-dash: the landing page's mood, ported to the skill grid ----------
   Loaded after style.css (see hub.html / index.html <head>), same convention
   about.css uses: reuse style.css's tokens (--ink-950, --crimson, --font-mono,
   --radius, ...), add only the new rules here. Every selector below targets
   classes hub.js already builds via createElement/textContent — this file adds
   no new elements and requires no hub.js change, so the untrusted-string /
   no-innerHTML rule in hub.js stays intact.

   Linked by FIVE pages, not one: hub.html, index.html, account.html,
   history.html, upgrade.html. index.html's #taskChooser reuses
   .hub-head/.hub-grid/.hub-card/.dash-head/.dash-skill/.dash-meta verbatim, so
   the Task 1/Task 2 picker cards pick up the same sticker treatment for free.
   The other three link it for .chip .label and .gauge-fill below — which, since
   v1.4.32 stripped the score readouts off the hub cards, no longer touch the
   hub at all: they now style /writing's summary card and /history's chips only.
   The filename is older than the truth; read the selectors, not the name. */

/* Eyebrow label: from a plain heavy Archivo label to the mono "technical
   register" /about and the login card use for every label on screen. */
.hub-eyebrow {
  font-family: var(--font-mono);
  font-weight: 600;
  letter-spacing: var(--tracking-label);
  color: var(--slate-500);
}

/* The sticker card: same graphic language as .login-card (style.css) and
   .about-letter (about.css) — hard ink border + flat offset shadow, no blur.
   Thinner offset than the login card's 9px since these are smaller, denser
   grid cards and a full 9px would out-compete the hero card above them. */
.hub-card {
  border: var(--border-hair) solid var(--line-crisp);
  border-radius: var(--radius-card);
  background: var(--surface-card);
  box-shadow: var(--shadow-card);
  transition: transform 0.14s ease, box-shadow 0.14s ease, border-color 0.14s ease;
}
/* Not built yet: same card shape, but the shadow drops to neutral grey so a
   locked skill never reads as an available action. */
/* Locked: the card must not read as an available action. With the offset
   shadow gone there is no coloured slab left to grey out, so the signal moves
   to the surface itself — it recedes rather than shouting. */
.hub-card.is-locked {
  background: var(--slate-50);
  box-shadow: var(--shadow-none);
}
/* Tactile lift, pointer devices only — gated by hover capability, never by
   prefers-reduced-motion (every dev machine here reports that on, so a
   reduced-motion query would just delete the effect for everyone). */
@media (hover: hover) {
  .hub-card:not(.is-locked):hover {
    transform: translateY(-2px);
    border-color: var(--line-ink);
    box-shadow: var(--shadow-lift);
  }
}
/* The narrow-screen block that used to shrink the offset shadow here is gone:
   a soft shadow does not need to be scaled down to fit a phone. */

/* Skill name: heavier Archivo weight, closer to how .about-headline commits
   to the display face rather than sharing weight with the meta text below it. */
.dash-skill {
  font-weight: var(--weight-black);
  text-transform: uppercase;
  letter-spacing: 0.01em;
}

/* Criteria chip labels (TR/CC/LR/GRA...) switch to mono, same register as the
   eyebrow; the numeric value stays Archivo/tabular so it still reads as data. */
.chip .label {
  font-family: var(--font-mono);
  font-size: var(--text-micro);
  font-weight: 600;
  letter-spacing: 0.04em;
  text-transform: uppercase;
}

/* Gauge fill: flat grey becomes an ink-to-crimson gradient, a restrained
   accent-color touch on a component that already exists rather than new
   decoration. Track stays --slate-200 so the "empty" portion is unchanged. */
.gauge-fill {
  background: linear-gradient(90deg, var(--ink-700), var(--crimson));
}

/* In-progress draft note: a left accent bar instead of plain text, the same
   "flagged" signal .about-mark gives a phrase, without needing the SVG
   turbulence-filter machinery for one persistent status line. hub.js still
   writes this as a single textContent node — no script change needed. */
.hub-draft-note {
  border-left: 3px solid var(--amber-300);
  padding-left: 10px;
}

/* The 64px technical-grid texture that sat behind the whole dashboard is gone.
   Both mockups rule it out in the same words — "không dùng nền lưới" — and the
   reason is specific rather than stylistic: the grid is the LANDING page's
   device, where it has one job for twenty seconds. Behind a dashboard of
   bordered cards it competes with every one of their 1px rules, and this
   screen now carries four stat tiles, two Task cards and a panel of progress
   bars where it carried two cards before.

   #hubView keeps position: relative — the stat and panel blocks below are
   positioned against it. */
#hubView { position: relative; }

/* ---------- the Writing dashboard (hub only) ----------
   Everything below is scoped under #hubView because this file is shared by
   five pages (see the header): index.html's #taskChooser reuses .hub-head /
   .hub-grid / .hub-card verbatim, so an unscoped rule here would reach it.
   Scoping also means no new stylesheet — and therefore no new entry in
   main.py's cache-stamp tuple for "/", which test_mechanics.py checks. */

/* The greeting. Sized for the LONGEST string it ever holds — "<tên>, nhớ ngủ
   sớm nhen", not a bare name — so --text-2xl rather than the --text-display
   the band readout uses; at 2.6rem that sentence eats two lines on a laptop
   and three on a phone. max-width keeps it breaking at a sane measure instead
   of running the full container width. */
#hubView .hub-name {
  font-family: var(--font-display);
  font-size: var(--text-2xl);
  font-weight: var(--weight-display);
  line-height: 1.15;
  letter-spacing: -0.01em;
  color: var(--navy);
  max-width: 20ch;
  margin: 4px 0 0;
}
/* The eyebrow is absent in the two late-night/early-morning windows (the
   greeting carries the name itself there), so the name must not depend on it
   for its top offset. */
#hubView #hubGreetLine.hidden { display: none; }

/* "Nạp thêm", inline at the end of the credit line — underlined rather than
   button-shaped: it is a way out of a dead end, not the action this page is
   asking for. */
#hubView .hub-topup {
  color: var(--crimson);
  font-weight: var(--weight-strong);
  text-decoration: underline;
}

/* The in-progress draft, full width above the two Task cards. Amber left rail
   like .hub-draft-note above, on the whole strip: while this is on screen both
   Task cards are locked, so it is the only live thing in the middle of the
   page and has to read that way. */
#hubView #hubDraft {
  border: var(--border-hair) solid var(--line-crisp);
  /* The amber rail STAYS heavy, and it is the only thing here that does. With
     both Task cards locked behind it this strip is the only live control on
     the page; the rail is what says so, and it now carries that job alone
     instead of sharing it with a 2.5px frame. */
  border-left: 4px solid var(--amber-300);
  border-radius: var(--radius-card);
  background: var(--surface-card);
  padding: 18px 22px;
  margin-bottom: 24px;
}

/* Recent lessons. Deliberately NOT a sticker card: no ink border, no offset
   shadow. Two of those already sit directly above it, and a third competing
   for the same weight would make the page read as three equal choices when
   only the Task cards are choices at all — this one is a rear-view mirror. */
#hubView .hub-recent {
  padding-top: 16px;
}
#hubView .hub-recent-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 12px;
  margin-bottom: 4px;
}
#hubView .hub-recent-all {
  background: none;
  border: 0;
  padding: 0;
  cursor: pointer;
  font-family: var(--font-body);
  font-size: var(--text-sm);
  font-weight: var(--weight-strong);
  color: var(--navy);
}
#hubView .hub-recent-all:hover { color: var(--crimson); }

/* One row per lesson: date, task, topic, band move. A <button>, not a link —
   same reason every other navigation on this page is location.href: the token
   is per-tab sessionStorage and a middle-click into a new tab would land on
   the login form. */
#hubView .hub-recent-row {
  display: flex;
  align-items: baseline;
  gap: 12px;
  width: 100%;
  padding: 10px 4px;
  background: none;
  border: 0;
  border-bottom: 1px solid var(--slate-100);
  text-align: left;
  cursor: pointer;
  font-family: var(--font-body);
  font-size: var(--text-sm);
  color: var(--slate-600);
}
#hubView .hub-recent-row:hover { background: var(--slate-50); }
#hubView .hub-recent-date {
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  color: var(--slate-500);
  flex: none;
}
#hubView .hub-recent-task {
  font-weight: var(--weight-strong);
  color: var(--navy);
  flex: none;
}
/* The topic is the only elastic column — it takes the slack and truncates,
   so the band on the right never gets pushed off the row by a long question. */
/* `flex: 0 1 auto`, not `1 1 auto`. Growing meant the exam question absorbed
   every spare pixel and shoved the band reading hard against the right edge
   with a lake of white between them — Marc asked for the two to sit closer.
   Now the row packs from the left and the band follows the text it belongs to.
   It still SHRINKS (and ellipsises) when the question is long. */
#hubView .hub-recent-topic {
  flex: 0 1 auto;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
#hubView .hub-recent-band {
  flex: none;
  font-weight: var(--weight-strong);
  color: var(--navy);
  font-variant-numeric: tabular-nums;
}

/* Phones: the row's four columns cannot hold one line, so date+task ride
   together on top and the topic drops below with the band beside it. */
@media (max-width: 640px) {
  #hubView .hub-name { font-size: var(--text-xl); max-width: none; }
  /* style.css pushes .dash-meta to the far edge on phones so a name and its
     meta read as two columns. That rule is written for .dash-head, a flex ROW
     — and on these cards the description is a direct child of the card's flex
     COLUMN instead, where margin-left:auto just shunts a short line to the
     right ("Bài luận nêu quan điểm" floating off Task 2's right edge). Undo it
     here only; #taskChooser on /writing still keeps its .dash-meta inside
     .dash-head, where the original rule is doing what it was written for. */
  #hubView .hub-card > .dash-meta { margin-left: 0; }
  #hubView #hubDraft { padding: 16px; }
  #hubView .hub-recent-row { flex-wrap: wrap; gap: 6px 10px; }
  #hubView .hub-recent-topic { flex-basis: 100%; order: 3; }
  #hubView .hub-recent-band { margin-left: auto; }
}


/* =====================================================================
   Dashboard: the four measures, and the criterion bars.
   ---------------------------------------------------------------------
   Calm Product UI, not the landing's language: 1px rules, flat white cards,
   no offset shadows, no grid ground. This screen is read before every lesson
   and has to stay quiet enough that the Task cards below it are still the
   loudest thing on the page — they are the only controls that start anything.
   ===================================================================== */

#hubView .hub-stats {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: 14px;
  margin: 22px 0 26px;
}
@media (min-width: 620px) { #hubView .hub-stats { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
/* Three, not four — the crimson "next exercise" tile left this row for the
   panels below. What is left is three counters, which is also more honest
   about what this row is. */
@media (min-width: 980px) { #hubView .hub-stats { grid-template-columns: repeat(3, minmax(0, 1fr)); } }

#hubView .hub-stat {
  display: flex;
  flex-direction: column;
  gap: 2px;
  padding: 16px 18px;
  background: var(--surface-card);
  border: var(--border-hair) solid var(--line-crisp);
  border-radius: var(--radius-card);
}

/* Mono, because these are labels on numbers. The same "technical register"
   the login card and the lesson chrome use. */
#hubView .hub-stat-label {
  font-family: var(--font-mono);
  font-weight: 600;
  font-size: 12px;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--slate-500);
}

#hubView .hub-stat-value {
  font-family: var(--font-display);
  font-weight: var(--weight-display);
  font-size: 30px;
  line-height: 1.15;
  color: var(--ink-950);
  /* Tabular, so a column of these does not jitter as the numbers change. */
  font-variant-numeric: tabular-nums;
}

#hubView .hub-stat-note {
  margin-top: 4px;
  font-size: 13px;
  font-weight: 500;
  line-height: 1.5;
  color: var(--slate-500);
}
#hubView .hub-stat-note.is-up { color: var(--emerald-600); font-weight: 600; }
#hubView .hub-stat-note.is-down { color: var(--crimson); font-weight: 600; }

/* The note panel. Still the only crimson fill on the screen — but it is no
   longer a control: the "Làm ngay →" button inside it is gone, because it led
   to /writing?task= for the task of the lesson that produced the advice, which
   is a recommendation nothing in the advice actually made.
   With that button gone the two "BẮT ĐẦU →" buttons on the Task cards are the
   only ways into a lesson from this page, which is what they should be. */
#hubView .hub-note {
  background: var(--crimson);
  border: var(--border-hair) solid var(--crimson);
  border-radius: var(--radius-card);
  padding: 18px 20px;
  align-self: start;
}
#hubView .hub-note .hub-eyebrow { color: rgba(255, 255, 255, 0.75); }

#hubView .hub-note-body {
  margin: 8px 0 0;
  font-family: var(--font-body);
  font-weight: 500;
  font-size: var(--text-sm);
  line-height: 1.6;
  color: #fff;
  /* next_focus is a model-written sentence of unbounded length. Clamped so one
     verbose lesson cannot make this column twice the height of its neighbours;
     six lines is far more room than it had as a stat tile (three). */
  display: -webkit-box;
  -webkit-line-clamp: 6;
  -webkit-box-orient: vertical;
  overflow: hidden;
}

/* ---------- the two panels ---------- */

#hubView .hub-panels {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: 22px;
  margin-top: 32px;
  border-top: var(--border-hair) solid var(--slate-200);
  padding-top: 6px;
}
/* 8 / 4 of twelve, as specified. One column below this width: the criterion
   bars need their full label to be readable, and a 4-of-12 column on a laptop
   is already close to the limit. */
/* Three columns now: recent lessons · the note · the criterion bars.
   5/3/4 rather than equal thirds — the lesson list carries the longest strings
   (a full exam question) and the criterion names are the longest fixed labels,
   so the note in the middle is the one that can afford to be narrowest. */
@media (min-width: 940px) {
  #hubView .hub-panels { grid-template-columns: 5fr 3fr 4fr; gap: 26px; }
}

#hubView .hub-blockers {
  padding-top: 16px;
  display: flex;
  flex-direction: column;
  gap: 14px;
}

#hubView .hub-blocker { display: flex; flex-direction: column; gap: 5px; }

#hubView .hub-blocker-top {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 10px;
}

#hubView .hub-blocker-name {
  font-size: var(--text-sm);
  font-weight: 600;
  color: var(--ink-950);
}

#hubView .hub-blocker-verdict {
  flex: none;
  font-family: var(--font-mono);
  font-weight: 600;
  font-size: 12px;
  white-space: nowrap;
}
#hubView .hub-blocker-verdict.is-bad { color: var(--crimson); }
#hubView .hub-blocker-verdict.is-mid { color: var(--amber-700); }
#hubView .hub-blocker-verdict.is-good { color: var(--emerald-600); }

#hubView .hub-blocker-track {
  height: 6px;
  background: var(--slate-200);
  border-radius: var(--radius-sm);
  overflow: hidden;
}
#hubView .hub-blocker-fill { height: 100%; border-radius: var(--radius-sm); }
#hubView .hub-blocker-fill.is-bad { background: var(--crimson); }
#hubView .hub-blocker-fill.is-mid { background: var(--amber-300); }
#hubView .hub-blocker-fill.is-good { background: var(--emerald-600); }

/* .hub-blocker-en is gone: the examiner's term used to sit UNDER a Vietnamese
   paraphrase, and it is the label itself now (see CRITERIA in hub.js). */

#hubView .hub-blocker-foot {
  margin: 6px 0 0;
  font-size: 12.5px;
  line-height: 1.5;
  color: var(--slate-400);
}


/* =====================================================================
   Dashboard skeleton — shown while /api/history/me is in flight.
   ---------------------------------------------------------------------
   Shapes match the real layout exactly (3 stat tiles, then 5fr/3fr/4fr),
   because a skeleton whose proportions disagree with the content makes the
   page jump at the moment the data arrives — the opposite of its job.

   No prefers-reduced-motion query (CLAUDE.md rule 4): the shimmer is the only
   thing distinguishing "loading" from "these cards are empty".
   ===================================================================== */

/* Margins copied from the elements each skeleton stands in for, not chosen to
   look right: #hubStats carries `margin: 22px 0 26px`, and leaving off that
   26px bottom margin put everything below the skeleton 26px too high — the
   page then dropped by that much the instant the data arrived. */
#hubView #hubSkeletonStats { margin: 22px 0 26px; }
#hubView .hub-skel.is-gone { display: none; }

#hubView .hub-skel-stats,
#hubView .hub-skel-panels {
  display: grid; grid-template-columns: minmax(0, 1fr); gap: 14px;
}
/* Matches .hub-panels' own top margin and rule, so the skeleton occupies the
   same vertical space its content will. */
#hubView .hub-skel-panels {
  margin-top: 32px; gap: 22px;
  border-top: var(--border-hair) solid var(--slate-200); padding-top: 22px;
}

@media (min-width: 620px) { #hubView .hub-skel-stats { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
@media (min-width: 980px) { #hubView .hub-skel-stats { grid-template-columns: repeat(3, minmax(0, 1fr)); } }
@media (min-width: 940px) { #hubView .hub-skel-panels { grid-template-columns: 5fr 3fr 4fr; gap: 26px; } }

#hubView .hub-skel-card {
  background: var(--surface-card);
  border: var(--border-hair) solid var(--line-crisp);
  border-radius: var(--radius-card);
  padding: 16px 18px;
  display: flex; flex-direction: column; gap: 10px;
}

/* Sizes only. The shimmer itself — background, animation and the skShimmer
   keyframes — moved to style.css as an unscoped `.sk`, because the writing
   page's resume skeleton needs the same primitive and two copies of it would
   drift. The widths below are deliberately uneven: a stack of identical bars
   reads as a broken grid rather than as text about to arrive. */
#hubView .sk-label { height: 11px; width: 45%; }
#hubView .sk-value { height: 30px; width: 32%; }
/* The stat tiles have THREE lines (label, number, note). Without this third
   bar the skeleton card was 53px shorter than the real one, so everything
   below it — the Task cards and the whole bottom row — sat too high and
   jumped down the moment the data arrived. Measured, not eyeballed. */
#hubView .sk-note  { height: 13px; width: 60%; }
#hubView .sk-row   { height: 14px; width: 100%; }
#hubView .sk-row:nth-of-type(3) { width: 86%; }
#hubView .sk-row:nth-of-type(4) { width: 68%; }

