/* ==========================================================================
   StudyHold — design tokens, reset, typography
   ========================================================================== */

:root {
  /* Brand ramp */
  --brand-50:  #eef2ff;
  --brand-100: #e0e7ff;
  --brand-200: #c7d2fe;
  --brand-300: #a5b4fc;
  --brand-400: #818cf8;
  --brand-500: #6366f1;
  --brand-600: #4f46e5;
  --brand-700: #4338ca;
  --brand-800: #3730a3;
  /* Brand colour when it carries *text* rather than a fill. --brand-500
     measures 4.46:1 on white — a hair under the 4.5:1 minimum, which axe
     reports as a serious violation. Fills keep --brand-500 so nothing
     changes visually where contrast isn't the constraint. */
  --brand-text: #595bd8;

  /* Semantic
     These are text colours as often as they are fills, so each one clears
     4.5:1 against every light surface below — white, --bg, --surface-2 and
     --surface-3. The original values sat between 3.2 and 3.7:1, which axe
     flags as a serious violation on every screen that shows a badge. The
     -strong variants keep the old, more saturated tone for fills and
     borders, where contrast with text is not the constraint. */
  --ok:      #117b38;
  --ok-strong: #16a34a;
  --ok-bg:   #dcfce7;
  --warn:    #a15804;
  --warn-strong: #d97706;
  --warn-bg: #fef3c7;
  --danger:  #ce2424;
  --danger-strong: #dc2626;
  --danger-bg:#fee9e9;   /* lifted slightly so --danger on it clears 4.5:1 */
  --info:    #06748e;
  --info-strong: #0891b2;
  --info-bg: #cffafe;

  /* U40 — workload heat, a light-to-heavy warm ramp for the calendar */
  --load-1-bg: #fef9ec;
  --load-2-bg: #fdedc8;

  /* Surfaces (light) */
  --bg:          #f6f7fb;
  --surface:     #ffffff;
  --surface-2:   #f1f3f9;
  --surface-3:   #e8ebf4;
  --border:      #e3e7f0;
  --border-strong:#cbd3e3;

  /* Text */
  --text:        #10141f;
  --text-2:      #4b5568;
  /* Muted text. #8791a5 measured 3.17:1 on white — under the 4.5:1 minimum —
     and it is the token behind .dim, .muted, .sub, .meta and .stat-label, so
     it alone accounted for around 500 contrast violations across all 21
     views. This value clears 4.5:1 on every light surface while still
     reading as clearly secondary. */
  --text-3:      #636b79;
  --text-invert: #ffffff;

  /* Effects */
  --radius-xs: 6px;
  --radius-sm: 9px;
  --radius:    13px;
  --radius-lg: 18px;
  --radius-full: 999px;

  --shadow-xs: 0 1px 2px rgba(15, 20, 35, .05);
  --shadow-sm: 0 1px 3px rgba(15, 20, 35, .07), 0 1px 2px rgba(15, 20, 35, .04);
  --shadow:    0 4px 12px rgba(15, 20, 35, .08), 0 1px 3px rgba(15, 20, 35, .05);
  --shadow-lg: 0 18px 46px rgba(15, 20, 35, .16), 0 4px 12px rgba(15, 20, 35, .08);

  --ring: 0 0 0 3px var(--brand-100);

  --sidebar-w: 244px;
  --topbar-h: 60px;

  --font: "Inter", ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto,
          "Helvetica Neue", Arial, sans-serif;
  --mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace;

  --ease: cubic-bezier(.4, 0, .2, 1);
}

[data-theme="dark"] {
  --bg:          #0c0f17;
  --surface:     #141926;
  --surface-2:   #1b2130;
  --surface-3:   #242c3d;
  --border:      #262e40;
  --border-strong:#38425a;

  --text:        #eef1f7;
  --text-2:      #a8b2c6;
  /* 4.5:1 against every dark surface. #6f7b93 measured 3.77-4.11. */
  --text-3:      #8993a6;

  --brand-50:  #1a1f3d;
  --brand-100: #232a52;
  --brand-200: #2f3a72;
  --brand-text: #8587f4;

  /* The light theme's semantic colours are darkened for contrast against a
     white page, which makes them *less* readable here — they have to be
     lightened against a dark one instead. Each clears 4.5:1 on --bg,
     --surface, --surface-2 and --surface-3. */
  --ok:        #22a853;
  --ok-strong: #16a34a;
  --warn:      #da7a0b;
  --warn-strong: #d97706;
  --danger:    #e76d6d;
  --danger-strong: #dc2626;
  --info:      #279fbc;
  --info-strong: #0891b2;

  --ok-bg:     #0f301d;   /* deepened so --ok on it clears 4.5:1 */
  --warn-bg:   #37280b;   /* deepened so --warn on it clears 4.5:1 */
  --danger-bg: #3d1618;
  --info-bg:   #0c2f38;

  --load-1-bg: #221d0c;
  --load-2-bg: #302711;

  --shadow-xs: 0 1px 2px rgba(0, 0, 0, .4);
  --shadow-sm: 0 1px 3px rgba(0, 0, 0, .5);
  --shadow:    0 4px 14px rgba(0, 0, 0, .5);
  --shadow-lg: 0 20px 50px rgba(0, 0, 0, .6);
  --ring: 0 0 0 3px var(--brand-200);
}

/* --------------------------------------------------------------- reset -- */

*, *::before, *::after { box-sizing: border-box; }

html, body {
  margin: 0;
  padding: 0;
  height: 100%;
}

body {
  background: var(--bg);
  color: var(--text);
  font-family: var(--font);
  font-size: 14px;
  line-height: 1.5;
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
  transition: background-color .2s var(--ease), color .2s var(--ease);
}

h1, h2, h3, h4, h5 { margin: 0; font-weight: 650; letter-spacing: -.015em; line-height: 1.25; }
h1 { font-size: 1.6rem; letter-spacing: -.025em; }
h2 { font-size: 1.15rem; }
h3 { font-size: 1rem; }
h4 { font-size: .875rem; }
p  { margin: 0; }

a { color: var(--brand-600); text-decoration: none; }
a:hover { text-decoration: underline; }

button, input, select, textarea { font: inherit; color: inherit; }
button { cursor: pointer; }
.btn-link { color: var(--brand-600); background: none; border: none; padding: 0; font-weight: 600; }
.btn-link:hover { text-decoration: underline; }

:focus-visible {
  outline: 2px solid var(--brand-500);
  outline-offset: 2px;
}

::-webkit-scrollbar { width: 10px; height: 10px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb {
  background: var(--border-strong);
  border-radius: var(--radius-full);
  border: 3px solid transparent;
  background-clip: content-box;
}
::-webkit-scrollbar-thumb:hover { background: var(--text-3); background-clip: content-box; }

/* ---------------------------------------------------------- utilities -- */

.muted    { color: var(--text-2); }
.dim      { color: var(--text-3); }
.tiny     { font-size: .72rem; }
.small    { font-size: .8rem; }
.bold     { font-weight: 650; }
.mono     { font-family: var(--mono); font-variant-numeric: tabular-nums; }
.nums     { font-variant-numeric: tabular-nums; }
.center   { text-align: center; }
.right    { text-align: right; }
.truncate { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.nowrap   { white-space: nowrap; }
.hidden   { display: none !important; }

.row      { display: flex; align-items: center; gap: 10px; }
.row-top  { display: flex; align-items: flex-start; gap: 10px; }
.between  { display: flex; align-items: center; justify-content: space-between; gap: 12px; }
.wrap     { flex-wrap: wrap; }
.grow     { flex: 1; min-width: 0; }
.col      { display: flex; flex-direction: column; }
.gap-2    { gap: 2px; }  .gap-4  { gap: 4px; }  .gap-6  { gap: 6px; }
.gap-8    { gap: 8px; }  .gap-12 { gap: 12px; } .gap-16 { gap: 16px; }
.gap-20   { gap: 20px; } .gap-24 { gap: 24px; }
.mt-4 { margin-top: 4px; }  .mt-8  { margin-top: 8px; }
.mt-12{ margin-top: 12px; } .mt-16 { margin-top: 16px; } .mt-24 { margin-top: 24px; }
.mb-4 { margin-bottom: 4px; } .mb-8 { margin-bottom: 8px; }
.mb-12{ margin-bottom: 12px; } .mb-16 { margin-bottom: 16px; }

.sr-only {
  position: absolute; width: 1px; height: 1px;
  padding: 0; margin: -1px; overflow: hidden;
  clip: rect(0, 0, 0, 0); white-space: nowrap; border: 0;
}

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: .01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: .01ms !important;
  }
}

/* ==========================================================================
   U09 — accent colour choice (Settings → Preferences)

   Only the brand ramp + ring shift; every other token (surfaces, text,
   semantic colors) stays exactly as designed. Applied via [data-accent] on
   <html>, alongside (never instead of) the existing theme tokens above.
   ========================================================================== */

[data-accent="teal"] {
  --brand-50: #ecfdf5; --brand-100: #d1fae5; --brand-200: #a7f3d0; --brand-300: #6ee7d5;
  --brand-400: #2dd4bf; --brand-500: #14b8a6; --brand-600: #0f766e; --brand-700: #115e59;
}
[data-theme="dark"][data-accent="teal"] {
  --brand-50: #0b2e29; --brand-100: #0f3e37; --brand-200: #155e52;
  --brand-400: #2dd4bf; --brand-500: #2dd4bf; --brand-600: #14b8a6; --brand-700: #0d9488;
}

[data-accent="rose"] {
  --brand-50: #fff1f2; --brand-100: #ffe4e6; --brand-200: #fecdd3; --brand-300: #fda4af;
  --brand-400: #fb7185; --brand-500: #e11d48; --brand-600: #be123c; --brand-700: #9f1239;
}
[data-theme="dark"][data-accent="rose"] {
  --brand-50: #3d0c17; --brand-100: #4c0f1d; --brand-200: #6b1526;
  --brand-400: #fb7185; --brand-500: #fb7185; --brand-600: #e11d48; --brand-700: #be123c;
}

[data-accent="amber"] {
  --brand-50: #fffbeb; --brand-100: #fef3c7; --brand-200: #fde68a; --brand-300: #fcd34d;
  --brand-400: #fbbf24; --brand-500: #d97706; --brand-600: #b45309; --brand-700: #92400e;
}
[data-theme="dark"][data-accent="amber"] {
  --brand-50: #3a2a0c; --brand-100: #4a350f; --brand-200: #6b4d16;
  --brand-400: #fbbf24; --brand-500: #f0a832; --brand-600: #d97706; --brand-700: #b45309;
}

/* ==========================================================================
   Study-shop accents — bought with study tokens (js/shop.js), applied by the
   same [data-accent] mechanism as the four that ship with the app. Only the
   brand ramp moves; surfaces, text and semantic colours are untouched.

   Every --brand-600 below clears 4.5:1 as text on white, --bg, --surface-2,
   --surface-3 and its own --brand-50, and carries white text at 4.5:1 as a
   button fill. Every --brand-300 (what dark mode uses for brand-coloured
   text) clears 4.5:1 on all four dark surfaces. The dark blocks re-step only
   50/100/200 — the tints that sit *behind* text — for the same reason the
   indigo default does.
   ========================================================================== */

[data-accent="violet"] {
  --brand-50: #f5f3ff; --brand-100: #ede9fe; --brand-200: #ddd6fe; --brand-300: #c4b5fd;
  --brand-400: #a78bfa; --brand-500: #8b5cf6; --brand-600: #6d28d9; --brand-700: #5b21b6;
}
[data-theme="dark"][data-accent="violet"] {
  --brand-50: #251a45; --brand-100: #2f2159; --brand-200: #43307d;
}

[data-accent="forest"] {
  --brand-50: #f0fdf4; --brand-100: #dcfce7; --brand-200: #bbf7d0; --brand-300: #86efac;
  --brand-400: #4ade80; --brand-500: #22c55e; --brand-600: #136b33; --brand-700: #0f5426;
}
[data-theme="dark"][data-accent="forest"] {
  --brand-50: #0b2818; --brand-100: #0f3520; --brand-200: #17512f;
}

[data-accent="ocean"] {
  --brand-50: #ecfeff; --brand-100: #cffafe; --brand-200: #a5f3fc; --brand-300: #67e8f9;
  --brand-400: #22d3ee; --brand-500: #0891b2; --brand-600: #0c6780; --brand-700: #0a5468;
}
[data-theme="dark"][data-accent="ocean"] {
  --brand-50: #082b33; --brand-100: #0b3843; --brand-200: #11525f;
}

[data-accent="graphite"] {
  --brand-50: #f6f7f9; --brand-100: #eceef2; --brand-200: #d8dce4; --brand-300: #b6bdca;
  --brand-400: #8b95a7; --brand-500: #5b6577; --brand-600: #414a5c; --brand-700: #333a49;
}
[data-theme="dark"][data-accent="graphite"] {
  --brand-50: #1e232d; --brand-100: #262c38; --brand-200: #333b4a;
}

/* ==========================================================================
   Study-shop skins — a surface re-tint, layered over light or dark exactly
   the way true-black and high-contrast are.

   Each colour below was generated by mixing the default surface toward a hue
   and then rescaling it back to the *same relative luminance*, so contrast is
   arithmetically what it was: every ratio in the app — text, muted text,
   semantic badges, every accent — is preserved to within 0.003 of the
   default's luminance. A skin changes the temperature of the app, never its
   readability. tests/shop.mjs asserts that, so a hand-picked colour cannot
   quietly break it later.
   ========================================================================== */

[data-theme="light"][data-skin="parchment"] {
  --bg: #fff5ef; --surface: #ffffff; --surface-2: #fff1e7; --surface-3: #f8e8de;
  --border: #f4e4da; --border-strong: #ded0ca;
}
[data-theme="dark"][data-skin="parchment"] {
  --bg: #160e02; --surface: #231706; --surface-2: #2c1f0c; --surface-3: #392916;
  --border: #3b2b18; --border-strong: #513f2f;
}

[data-theme="light"][data-skin="glacier"] {
  --bg: #f4f7ff; --surface: #ffffff; --surface-2: #eef3fe; --surface-3: #e2ecff;
  --border: #dae8ff; --border-strong: #bfd4f4;
}
[data-theme="dark"][data-skin="glacier"] {
  --bg: #02101e; --surface: #051b2d; --surface-2: #092338; --surface-3: #102e48;
  --border: #12304b; --border-strong: #224466;
}

[data-theme="light"][data-skin="orchid"] {
  --bg: #f7f6fe; --surface: #ffffff; --surface-2: #f2f2fe; --surface-3: #ebe9ff;
  --border: #e7e4ff; --border-strong: #d4cdfd;
}
[data-theme="dark"][data-skin="orchid"] {
  --bg: #170727; --surface: #230f39; --surface-2: #2d1546; --surface-3: #391e58;
  --border: #3b205b; --border-strong: #503379;
}

/* True black is a deliberate choice about OLED power, so it wins over a
   bought skin's dark surfaces rather than fighting them. */
[data-theme="dark"][data-trueblack="1"][data-skin] {
  --bg: #000000; --surface: #000000; --surface-2: #0a0a0a; --surface-3: #131313;
  --border: #1c1c1c;
}

/* ==========================================================================
   Study-shop avatar rings — worn only on your OWN avatar (the sidebar
   profile chip, and the preview swatch in the shop), never on a classmate's
   or a contact's, which are identity, not decoration.
   ========================================================================== */

.profile-chip .avatar, .ring-preview .avatar { position: relative; }

[data-ring="gold"] .profile-chip .avatar,
.ring-preview[data-ring-preview="gold"] .avatar {
  box-shadow: 0 0 0 2px var(--surface), 0 0 0 4px #d4a017;
}

[data-ring="neon"] .profile-chip .avatar,
.ring-preview[data-ring-preview="neon"] .avatar {
  box-shadow: 0 0 0 2px var(--surface), 0 0 0 4px var(--brand-500), 0 0 12px var(--brand-400);
}

/* The gradient is a masked *band*, not a disc behind the avatar.

   Two earlier shapes of this were wrong in instructive ways. Putting the
   conic gradient on the avatar's own background covers the colour UI.avatar()
   picked to carry the white initials at 4.5:1 — a decoration eating a
   contrast guarantee. Putting it on a pseudo-element at z-index:-1 paints it
   below every in-flow background that comes after it in the painting order,
   including the card the preview sits on, so the ring simply vanished.

   Masking out the middle means the pseudo-element can paint on top, where
   nothing can hide it, while the avatar's face and initials show through the
   hole untouched. */
[data-ring="aurora"] .profile-chip .avatar::after,
.ring-preview[data-ring-preview="aurora"] .avatar::after {
  content: "";
  position: absolute;
  inset: -4px;
  border-radius: 50%;
  background: conic-gradient(#22d3ee, #a78bfa, #f472b6, #fbbf24, #22d3ee);
  -webkit-mask: radial-gradient(closest-side, transparent calc(100% - 4px), #000 calc(100% - 3.2px));
          mask: radial-gradient(closest-side, transparent calc(100% - 4px), #000 calc(100% - 3.2px));
  animation: auroraSpin 6s linear infinite;
  pointer-events: none;
}

/* Without mask support the band would be a filled disc over the initials, so
   the ring degrades to a solid one instead. */
@supports not ((-webkit-mask: radial-gradient(#000, #000)) or (mask: radial-gradient(#000, #000))) {
  [data-ring="aurora"] .profile-chip .avatar::after,
  .ring-preview[data-ring-preview="aurora"] .avatar::after {
    background: none;
    box-shadow: 0 0 0 3px #a78bfa;
    animation: none;
  }
}

@keyframes auroraSpin { to { transform: rotate(360deg); } }

/* The spin is decoration with no information in it — the ring itself still
   renders, it just stops moving. */
@media (prefers-reduced-motion: reduce) {
  [data-ring="aurora"] .profile-chip .avatar::after,
  .ring-preview[data-ring-preview="aurora"] .avatar::after { animation: none; }
}

/* ============================================================ U46 — CVD preview
   Applies a colour-vision-deficiency simulation filter to the whole app so a
   class-colour or chart choice can be checked before it ships. Off by default. */
[data-cvd="protanopia"]  { filter: url(#cvd-protanopia); }
[data-cvd="deuteranopia"] { filter: url(#cvd-deuteranopia); }
[data-cvd="tritanopia"]  { filter: url(#cvd-tritanopia); }

/* ==================================================== U11 — type size + dyslexia-friendly face
   Most of the app's type is set in literal px (not rem), so scaling the root
   font-size cascades nowhere. `zoom` scales the whole shell — fonts, icons,
   spacing, and hit targets together — and unlike transform:scale it actually
   reflows layout, so nothing overlaps or gets clipped. */
[data-fontsize="large"]  body { zoom: 1.12; }
[data-fontsize="larger"] body { zoom: 1.25; }
[data-dyslexic="1"] {
  --font: Verdana, Tahoma, "Trebuchet MS", sans-serif;
  letter-spacing: .01em;
  word-spacing: .06em;
}
[data-dyslexic="1"] body { line-height: 1.65; }

/* ============================================================ U13 — true black
   An OLED-friendly layer on top of dark mode: pure black surfaces save real
   battery, without touching any other token (text, brand, semantic colors
   stay exactly as designed). Only takes effect while dark mode is active. */
[data-theme="dark"][data-trueblack="1"] {
  --bg: #000000;
  --surface: #000000;
  --surface-2: #0a0a0a;
  --surface-3: #131313;
  --border: #1c1c1c;
}

/* ================================================================ U12
   High contrast — stronger text, borders, and focus rings for low vision
   or a bright outdoor screen. Layers over either theme without touching
   layout, exactly like true-black does for dark mode. */
[data-contrast="high"] {
  --text: #000000;
  --text-2: #141414;
  --text-3: #3a3a3a;
  --border: #000000;
  --border-strong: #000000;
  --surface-2: #ececec;
  --surface-3: #dcdcdc;
  --danger: #b30000;
  --warn: #8a5200;
  --ok: #0b6e2c;
}
[data-theme="dark"][data-contrast="high"] {
  --text: #ffffff;
  --text-2: #f2f2f2;
  --text-3: #cfcfcf;
  --border: #ffffff;
  --border-strong: #ffffff;
  --surface-2: #1a1a1a;
  --surface-3: #292929;
  --danger: #ff6b6b;
  --warn: #ffc24d;
  --ok: #5ce08a;
}
[data-contrast="high"] .btn,
[data-contrast="high"] .input,
[data-contrast="high"] .select,
[data-contrast="high"] .textarea,
[data-contrast="high"] .check {
  border-width: 2px;
}
[data-contrast="high"] :focus-visible {
  outline: 3px solid var(--brand-600);
  outline-offset: 2px;
}

/* -------------------------------------------------------- prismatic ring --
   The Elite ring, in the same idiom as gold/neon/aurora above: a masked band
   on a pseudo-element, so it paints on top without covering the initials that
   UI.avatar() sized for 4.5:1. Where aurora spins one gradient, this cycles
   the band through the app's accent hues. */
[data-ring="prismatic"] .profile-chip .avatar::after,
.ring-preview[data-ring-preview="prismatic"] .avatar::after {
  content: "";
  position: absolute;
  inset: -4px;
  border-radius: 50%;
  background: #e11d48;
  -webkit-mask: radial-gradient(closest-side, transparent calc(100% - 4px), #000 calc(100% - 3.2px));
          mask: radial-gradient(closest-side, transparent calc(100% - 4px), #000 calc(100% - 3.2px));
  animation: prismaticCycle 6s linear infinite;
  pointer-events: none;
}

/* Without mask support the band would cover the initials, so it degrades to a
   solid ring — the same fallback aurora takes. */
@supports not ((-webkit-mask: radial-gradient(#000, #000)) or (mask: radial-gradient(#000, #000))) {
  [data-ring="prismatic"] .profile-chip .avatar::after,
  .ring-preview[data-ring-preview="prismatic"] .avatar::after {
    background: none;
    box-shadow: 0 0 0 3px #7c3aed;
    animation: none;
  }
}

@keyframes prismaticCycle {
  0%   { background: #e11d48; }
  25%  { background: #d97706; }
  50%  { background: #059669; }
  75%  { background: #2563eb; }
  100% { background: #e11d48; }
}

@media (prefers-reduced-motion: reduce) {
  [data-ring="prismatic"] .profile-chip .avatar::after,
  .ring-preview[data-ring-preview="prismatic"] .avatar::after { animation: none; }
}
