/* ============================================================
   DESIGN TOKENS
   "Variables" for colour, spacing, etc. Defined once here, reused
   everywhere with var(--name). Change a value here and the whole
   site updates — this is how real design systems (incl. Apple's)
   stay consistent.
   ============================================================ */
:root {
  --bg:          #f5f5f7;   /* Apple's signature light grey */
  --surface:     #ffffff;   /* cards & sidebar */
  --ink:         #1d1d1f;   /* near-black text */
  /* TRUE BLACK, for the few places that must read as black rather than as
     "the site's dark grey". --ink is #1d1d1f — a deliberate near-black
     that keeps long body copy from feeling harsh — but on a short, bold
     label sitting on white it reads as grey, which is exactly the
     complaint that put this token here. Reach for --ink first; use this
     only where a name or number has to look definitively black. */
  --ink-strong:  #000000;   /* headings that must read black, not grey */
  --ink-soft:    #6e6e73;   /* secondary text */
  --line:        #e5e5ea;   /* hairline borders */
  --accent:      #0071e3;   /* Apple blue */
  --accent-soft: #e8f1fd;
  /* Meter fill — a muted sage, not a signal green. It reads as a measured
     quantity rather than a "pass"; a saturated green would make ten bars
     the loudest thing on a card whose subject is the skill names. Kept as
     a token, like every other colour here, so the dark theme can restate
     it once below instead of every rule that uses it carrying an override.

     Darker than it first looks like it needs to be, deliberately: the fill
     has to be distinguishable from the TRACK BEHIND IT, and the track is
     the pale --line. A prettier, lighter sage measured 2.7:1 against it —
     under the 3:1 floor for a non-text graphic — which means a bar whose
     empty half you can barely find, and a proportion you can't read. */
  --meter:       #3f8362;
  /* THE SOFTENED FILL — the same sage at 90%, for the two progress bars.
     Solid, it was the heaviest thing on a card whose subject is the name
     above it.

     90% AND NOT LESS, and the reason is specific: a transparent fill
     blends with WHAT IS BEHIND IT, and what is behind it is the track —
     so every step of alpha mixes the fill toward the one thing it has to
     stay distinguishable from. Measured against --line:

       100%  3.61:1     90%  3.11:1     80%  2.69:1     60%  2.06:1

     80% is already under the floor. 90% is the whole budget the current
     track affords, which is why the bar was ALSO thinned — height costs
     nothing to the ratio and did most of the visual work.

     Its own token rather than an alpha written into each rule, so the
     dark theme restates it once. --meter itself stays solid: the tool
     card's rung segments use it, they sit on the card rather than on a
     track, and none of this reasoning applies to them. */
  --meter-soft:  rgba(63, 131, 98, .9);

  /* ---- THE SCORE RAMP: five bands of twenty, red to green ----

     An ABSOLUTE scale, cut at fixed twenties — not thirds of his own
     range. A relative scale would paint whatever his weakest skill
     happens to be with the worst colour, forever, however good that
     skill actually was. His lowest is 45, and 45 is not red.

     MUTED, NOT SIGNAL. Maximising saturation produced #fe0607 and
     #03fe49; this site's own sage measures 8.7 chroma and its accent
     blue 19.3, so the red here is 17.8 — clearly red, still under the
     loudest colour the site already uses.

     EVERY STEP CLEARS 3:1 against its own theme's track, at the 90%
     alpha the bars paint with (light: 4.56 / 3.47 / 3.25 / 3.22 / 3.46).
     That constraint is why the "light lime green" originally asked for
     is not here: light colours vanish on a pale track — a bright lime
     measured 1.26:1 — so the best scores would have been the faintest
     bars on the card, weight running opposite to meaning.

     ADJACENT STEPS ARE CLOSE ON PURPOSE (7-10 apart in OKLab). This is a
     SEQUENTIAL scale, not a categorical palette: neighbouring steps of
     one quantity are meant to resemble each other. What has to be
     unmistakable is the ENDS, and they are 26.0 apart — which survives
     deuteranopia at 26.2, because the ramp carries lightness as well as
     hue. Colour is a third channel here anyway; the bar's length and the
     printed number already state the value. */
  --band-1:      rgba(179, 38, 30, .9);    /*  0-19   red          */
  --band-2:      rgba(168, 90, 26, .9);    /* 20-39   burnt amber  */
  --band-3:      rgba(138, 112, 22, .9);   /* 40-59   dark gold    */
  --band-4:      rgba(95, 125, 36, .9);    /* 60-79   olive green  */
  --band-5:      rgba(47, 125, 67, .9);    /* 80-100  green        */

  --radius:      18px;
  --radius-sm:   12px;
  --shadow:      0 1px 3px rgba(0,0,0,.04), 0 8px 24px rgba(0,0,0,.05);
  --sidebar-w:      300px;
  --sidebar-mini-w: 96px;   /* width when collapsed to icons only */
}

/* DARK THEME — just re-defines the colour tokens. Because everything else
   uses var(--name), the whole site flips automatically. Applied when
   <html data-theme="dark"> is set (toggled by JS). */
[data-theme="dark"] {
  --bg:          #1c1c1e;   /* near-black background */
  --surface:     #2c2c2e;   /* cards & sidebar (slightly lighter) */
  --ink:         #f5f5f7;   /* near-white text */
  /* INVERTS, like every token here. Pure #fff is to a dark card what pure
     #000 is to a white one: the strongest the theme can go. Without this
     line a literal black would have survived the theme flip and turned
     the skill names invisible — the reason this is a token at all. */
  --ink-strong:  #ffffff;   /* the dark theme's strongest ink */
  --ink-soft:    #98989d;   /* secondary text */
  --line:        #3a3a3c;   /* borders */
  --accent:      #0a84ff;   /* Apple's brighter dark-mode blue */
  --accent-soft: rgba(10,132,255,.18);
  /* Lifted, not re-hued: the same sage needs more light to hold its
     weight against a dark card, exactly as --accent brightens here. */
  --meter:       #5cb188;
  /* The same 90%, over the dark theme's own track: 3.85:1, comfortably
     clear. Dark has more room than light does here — the lifted sage
     starts further from #3a3a3c than the deep one does from #e5e5ea. */
  --meter-soft:  rgba(92, 177, 136, .9);
  /* THE SCORE RAMP, LIFTED FOR THE DARK TRACK. Not the light steps
     reused: on #3a3a3c a colour must be LIGHT to be visible, the exact
     inverse of the light theme's requirement. Same five hues, same
     order, restated — at 90% these measure 3.17 / 3.92 / 5.18 / 5.21 /
     4.64, and the ends stay 26.9 apart. */
  --band-1:      rgba(232, 105, 95, .9);
  --band-2:      rgba(224, 144, 74, .9);
  --band-3:      rgba(214, 187, 82, .9);
  --band-4:      rgba(166, 201, 95, .9);
  --band-5:      rgba(99, 197, 138, .9);
}

/* ---- Dark-mode frosted glass ----
   In dark mode (and ONLY dark mode) we give the page a deep, subtly colourful
   gradient and make the sidebar translucent + blurred — the look from the
   reference image. Light mode stays clean solid white. */
[data-theme="dark"] body {
  background: #121214;   /* clean neutral dark — no colour tint */
}
[data-theme="dark"] .sidebar {
  background: rgba(40, 40, 46, .55);   /* see-through so the gradient shows */
  backdrop-filter: saturate(160%) blur(26px);
  -webkit-backdrop-filter: saturate(160%) blur(26px);
  border-right: 1px solid rgba(255, 255, 255, .08);
}
/* hover tints that suit glass instead of the solid light-mode greys */
[data-theme="dark"] .nav-item:hover,
[data-theme="dark"] .profile-chip:hover { background: rgba(255, 255, 255, .07); }

/* Reset: strip the browser's default margins so our spacing is
   intentional, and make box-sizing predictable. */
* { margin: 0; padding: 0; box-sizing: border-box; }

body {
  /* The Apple font stack — uses the OS's own system font. */
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
  /* Clean Apple-style light grey. White cards & sidebar sit on top of it. */
  background: var(--bg);
  color: var(--ink);
  -webkit-font-smoothing: antialiased;
  line-height: 1.5;
  min-height: 100vh;
  /* smooth fade when switching light/dark */
  transition: background-color .25s ease, color .25s ease;
}

/* ============================================================
   SIDEBAR
   ============================================================ */
.sidebar {
  position: fixed;
  /* Flush to the left edge, full height — part of the page, not floating. */
  top: 0; left: 0; bottom: 0;
  width: var(--sidebar-w);
  /* Uses the surface token so it flips with dark mode. */
  background: var(--surface);
  border-right: 1px solid var(--line);
  padding: 22px 16px 30px;
  display: flex;
  flex-direction: column;
  /* Still scrollable if the screen is short, but the bar is hidden for a
     cleaner, more minimalist look. */
  overflow-y: auto;
  overflow-x: hidden;
  scrollbar-width: none;        /* Firefox */
  transition: width .22s ease;  /* smooth shrink/grow */
}
.sidebar::-webkit-scrollbar { display: none; }  /* Chrome, Edge, Safari */

/* Circular collapse/expand button straddling the sidebar's right edge.
   Fixed to the viewport; its left position sits on the sidebar border
   (sidebar width minus half the button), and follows when width changes. */
.sidebar-toggle {
  position: fixed;
  top: 34px;
  left: calc(var(--sidebar-w) - 14px);
  z-index: 20;
  display: flex;
  align-items: center;
  justify-content: center;
  width: 28px;
  height: 28px;
  border-radius: 50%;
  background: var(--surface);
  border: 1px solid var(--line);
  box-shadow: 0 1px 3px rgba(0,0,0,.10);
  color: var(--ink-soft);
  cursor: pointer;
  transition: left .22s ease, background .15s ease, color .15s ease;
}
.sidebar-toggle:hover { background: var(--bg); color: var(--ink); }
.sidebar-toggle svg { width: 16px; height: 16px; transition: transform .22s ease; }

/* Still used to highlight "mlbb" in strivemlbb.com */
.brand-accent { color: var(--accent); }

.nav { display: flex; flex-direction: column; gap: 4px; flex: 1; }

.nav-label {
  display: flex;
  align-items: center;
  justify-content: space-between;   /* pushes the chevron to the right edge */
  width: 100%;
  font-size: 13px;
  font-weight: 400;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--ink);
  padding: 20px 10px 8px;
  /* it's now a <button>, so strip the default button look */
  background: transparent;
  border: none;
  font-family: inherit;
  cursor: pointer;
}

/* The little fold-arrow next to each group label. */
.nav-chevron {
  width: 14px;
  height: 14px;
  color: var(--ink-soft);
  transition: transform .2s ease;   /* smooth rotate when toggled */
}
/* When a group is collapsed: rotate the arrow and hide its items. */
.nav-group.is-collapsed .nav-chevron { transform: rotate(-90deg); }
.nav-group.is-collapsed .nav-items { display: none; }

/* Keeps the spacing between items inside a group. */
.nav-items { display: flex; flex-direction: column; gap: 4px; }

/* Settings sits at the bottom (nav has flex:1, pushing it down).
   Dark mode used to live here as its own toggle; it now lives inside
   the Settings section, so this is a normal nav item. */
/* A DIVIDER ABOVE SETTINGS, drawn in the gap rather than on the button.

   `border-top` on .nav-settings itself would put a straight line across
   an element with a rounded hover background — and since a background
   paints under its own border, hovering Settings would slide a grey
   panel up against the line. A pseudo-element sits in the MARGIN, so
   the button keeps its own shape and the rule never moves.

   Centred in the gap: 14px of margin, the line at -7px. */
.nav-settings { position: relative; margin-top: 14px; }
.nav-settings::before {
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  top: -7px;
  border-top: 1px solid var(--line);
}

.nav-item {
  display: flex;
  align-items: center;
  gap: 12px;
  width: 100%;
  padding: 13px 14px;
  border: none;
  background: transparent;
  border-radius: var(--radius-sm);
  font: inherit;
  font-size: 16px;
  font-weight: 400;
  color: var(--ink);
  text-align: left;
  cursor: pointer;
  transition: background .15s ease, transform .15s ease;
}
.nav-item:hover { background: var(--bg); }
.nav-item.is-active {
  /* soft blue tint — the clean Apple way to show a selected item */
  background: var(--accent-soft);
  color: var(--accent);
}
/* Lucide renders an <svg>; size it down from its 24px default. */
/* ---- NAV TILES: an app-icon square per item ----

   A rounded square with a colour fill and the glyph knocked out in
   white — the shape a home-screen app icon has. The tile is a WRAPPER
   span, not the icon: Lucide swaps `<i data-lucide>` for an `<svg>`,
   and a background with padding on an inline SVG is something browsers
   disagree about. A span is boring and behaves.

   Every colour below was measured against white before it was used —
   a white glyph on a coloured tile is a graphic carrying meaning, so
   WCAG 1.4.11's 3:1 floor applies, the same rule the skill bars are
   held to. The weakest pairing here is Diet at 3.23:1; a prettier
   pastel would leave the glyph unreadable at 18px. */
.nav-tile {
  width: 32px;
  height: 32px;
  /* 23%, not a fixed 9px. iOS corners are a superellipse at roughly
     22% of the icon's size — a percentage keeps that ratio if the tile
     is ever resized, where a pixel value silently changes the shape. */
  border-radius: 23%;
  flex-shrink: 0;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  color: #fff;                 /* the glyph, not the tile */
  background: var(--ink-soft); /* fallback: a new item shows grey, not nothing */
  /* FOUR SHADOWS, each doing one job:
       inset top    a 1px highlight along the upper edge — the light
                    catching a raised surface. A 1px LINE, not a wash
                    over the face: washing it would lighten the area
                    the white glyph sits on and eat the contrast the
                    gradient was built to protect.
       inset bottom the matching shade underneath.
       0 1px 2px    the contact shadow that seats it on the strip.
       0 2px 6px    the soft ambient one that gives it height. */
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, .22),
              inset 0 -1px 0 rgba(0, 0, 0, .12),
              0 1px 2px rgba(0, 0, 0, .16),
              0 2px 6px rgba(0, 0, 0, .08);
}
/* The glyph keeps its own stroke weight rather than being scaled up to
   fill the tile — 18px inside 32px leaves the padding an icon needs. */
/* 18 of 32 is ~56% — the share of the tile an app glyph usually takes.

   stroke-width is raised from Lucide's 2 because that is tuned for a
   24px icon standing on its own; drawn at 18px inside a coloured tile
   it renders as a thin scratch. 2.4 gives it the weight a filled iOS
   glyph has without redrawing fifteen icons. CSS wins over the SVG's
   own stroke-width attribute, so no markup changes.

   The drop-shadow is what stops white-on-mid-tone looking flat. */
.nav-tile svg {
  width: 18px;
  height: 18px;
  display: block;
  stroke-width: 2.4;
  filter: drop-shadow(0 1px 1px rgba(0, 0, 0, .22));
}

/* KEYED ON data-target, NOT ON A CLASS PER ITEM. The target id is
   already unique and already in the markup, so a tile's colour cannot
   drift away from the item it belongs to — rename the target and the
   colour follows, or visibly stops applying. */
.nav-item[data-target="weight"] .nav-tile {
  background-image: linear-gradient(180deg, #d85f39, #ad4c2e);
}
.nav-item[data-target="sport"] .nav-tile {
  background-image: linear-gradient(180deg, #d94f6a, #ae3f55);
}
.nav-item[data-target="diet"] .nav-tile {
  background-image: linear-gradient(180deg, #4e9036, #3e732b);
}
.nav-item[data-target="money"] .nav-tile {
  background-image: linear-gradient(180deg, #2f8f5b, #267249);
}
.nav-item[data-target="investment"] .nav-tile {
  background-image: linear-gradient(180deg, #2f6fd0, #2659a6);
}
.nav-item[data-target="web-dev"] .nav-tile {
  background-image: linear-gradient(180deg, #4a5bd4, #3b49aa);
}
.nav-item[data-target="entrepreneurship"] .nav-tile {
  background-image: linear-gradient(180deg, #7a4fd0, #623fa6);
}
.nav-item[data-target="self-care"] .nav-tile {
  background-image: linear-gradient(180deg, #c4527f, #9d4266);
}
.nav-item[data-target="bedroom"] .nav-tile {
  background-image: linear-gradient(180deg, #5b6b8a, #49566e);
}
.nav-item[data-target="islam"] .nav-tile {
  background-image: linear-gradient(180deg, #1f7f78, #196660);
}
.nav-item[data-target="vehicle"] .nav-tile {
  background-image: linear-gradient(180deg, #b3711c, #8f5a16);
}
.nav-item[data-target="property"] .nav-tile {
  background-image: linear-gradient(180deg, #a2662c, #825223);
}
.nav-item[data-target="album"] .nav-tile {
  background-image: linear-gradient(180deg, #b4478f, #903972);
}
.nav-item[data-target="travel-map"] .nav-tile {
  background-image: linear-gradient(180deg, #1f8aa8, #196e86);
}
/* Settings is the system app, so it takes the system grey rather than a
   hue of its own — it is not one of the places you keep things. */
.nav-item[data-target="settings"] .nav-tile {
  background-image: linear-gradient(180deg, #6e6e73, #58585c);
}

.sidebar-footer { padding-top: 10px; border-top: 1px solid var(--line); }
.date { font-size: 12px; color: var(--ink-soft); padding: 0 10px; }

/* Profile chip at the top of the sidebar.
   This is a WRAPPER holding two independent buttons: the name block
   (→ Home) and the progress ring (→ Weight). So the wrapper itself gets
   no cursor and no hover — each button owns its own feedback, which is
   what tells you they're two different targets. */
.profile-chip {
  display: flex;
  align-items: center;
  width: 100%;
  padding: 6px 6px 12px;
  margin-bottom: 2px;
}
/* The name block: this is the part that behaves like the old chip. */
.profile-chip-main {
  flex: 1;                 /* eat the free space so the ring sits far right */
  min-width: 0;            /* lets long names ellipsis instead of overflowing */
  display: flex;
  align-items: center;     /* name+role block centered next to the avatar */
  gap: 11px;
  padding: 5px 6px;
  border: none;
  background: transparent;
  border-radius: var(--radius-sm);
  font: inherit;
  text-align: left;
  cursor: pointer;
  transition: background .15s ease;
}
.profile-chip-main:hover { background: var(--bg); }
.profile-meta { display: flex; flex-direction: column; justify-content: center; line-height: 1.25; }
.profile-name { font-size: 17px; font-weight: 500; color: var(--ink); }
.profile-role { font-size: 14px; font-weight: 400; color: var(--ink-soft); }

/* Weight-progress ring — a BUTTON that opens the Weight section.
   --progress = % of the way from your start weight to your target.
   weight.js sets it from real weigh-ins (it used to be a fake level).
   Note there is no `background` in the button reset below: that would
   wipe out the conic-gradient which IS the ring. */
.level-badge {
  --progress: 0;
  position: relative;
  margin-left: 8px;             /* a comfortable gap after the name/role */
  display: flex;
  align-items: center;
  justify-content: center;
  width: 32px;
  height: 32px;
  border-radius: 50%;
  flex-shrink: 0;
  border: none;
  padding: 0;
  font: inherit;
  cursor: pointer;
  /* conic-gradient = the ring: accent blue up to --progress%, faint track after */
  background: conic-gradient(var(--accent) calc(var(--progress) * 1%), var(--line) 0);
  transition: transform .15s ease, box-shadow .15s ease;
}
.level-badge:hover { transform: scale(1.08); }
.level-badge:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
/* Nothing logged yet — sit quietly instead of implying 0% progress. */
.level-badge.is-empty { background: var(--line); }
.level-badge.is-empty .level-num { color: var(--ink-soft); }
/* Target reached: fill the whole ring with the success green. */
.level-badge.is-done { background: #0ca30c; }

/* the cut-out centre, leaving only a thin ring visible */
.level-badge::before {
  content: "";
  position: absolute;
  inset: 3px;
  border-radius: 50%;
  background: var(--surface);
}
/* the number sits above the cut-out */
.level-num {
  position: relative;
  font-size: 11px;
  font-weight: 500;
  color: var(--ink);
  font-variant-numeric: tabular-nums;
}
/* three digits (100%) need to shrink to fit the 26px cut-out */
.level-num.is-long { font-size: 9px; }

/* The lead group (Sport/Diet) has no label, so it needs its own bottom
   gap to separate it from the FINANCE label underneath. */
.nav-group-lead { margin-bottom: 4px; }

/* Avatar — a circular frame that CLIPS the photo. Reused at two sizes. */
.avatar {
  width: 40px;
  height: 40px;
  border-radius: 50%;
  flex-shrink: 0;
  overflow: hidden;          /* crops anything outside the circle */
  background: var(--bg);     /* shows while the image loads */
}
/* The photo fills the frame, then scales up and shifts toward the face,
   pushing the white background & shirt out of view. */
.avatar img {
  width: 100%;
  height: 100%;
  object-fit: cover;
}

/* ---- the avatar.png framing fix ----
   avatar.png has a lot of empty space above the head, so it needs zooming
   in to look right in a small circle. That zoom must NOT apply to a photo
   you uploaded: you already framed that one yourself in the adjuster, and
   scaling it again crops your choice a second time (the chip looked
   over-zoomed while the Settings preview looked correct).

   identity.js puts .has-custom-photo on <html> when a photo is stored, so
   this only ever touches the bundled file. */
:root:not(.has-custom-photo) .avatar img,
:root:not(.has-custom-photo) .profile-photo img {
  transform: scale(1.65);
  transform-origin: 50% 42%;  /* anchored lower so the white above the head crops out */
}
.avatar-lg { width: 80px; height: 80px; }

/* ============================================================
   MAIN CONTENT
   ============================================================ */
.main {
  margin-left: var(--sidebar-w);
  /* top padding matches the sidebar's (22px) so the hero lines up with the
     sidebar profile chip */
  padding: 22px 56px 80px;
  /* NO max-width here any more — see .view below. It used to cap at
     1100px, which on a wide screen left a third of the window empty
     down the right-hand side and squeezed the card grid to three
     columns, orphaning the fourth card of a four-card section. */
  transition: margin-left .22s ease;   /* glides when the sidebar shrinks */
}

/* ============================================================
   COLLAPSED (icon-only) SIDEBAR
   Added when .sidebar has the .is-mini class (toggled by JS).
   ============================================================ */
.sidebar.is-mini { width: var(--sidebar-mini-w); padding-left: 10px; padding-right: 10px; }
/* button is a sibling now, so target it with ~ and move it to the narrow edge */
.sidebar.is-mini ~ .sidebar-toggle { left: calc(var(--sidebar-mini-w) - 14px); }
.sidebar.is-mini ~ .sidebar-toggle svg { transform: rotate(180deg); }  /* arrow points right */

/* Hide all the text — names, roles, group labels, item labels, level badge */
.sidebar.is-mini .profile-meta,
.sidebar.is-mini .level-badge,
.sidebar.is-mini .nav-label { display: none; }
.sidebar.is-mini .profile-chip { justify-content: center; padding: 6px 0 16px; }
/* the inner button stops stretching, so the avatar centres properly */
.sidebar.is-mini .profile-chip-main { flex: none; justify-content: center; gap: 0; }
/* font-size: 0 collapses the text label; the SVG icon is sized in px so it stays */
.sidebar.is-mini .nav-item { justify-content: center; gap: 0; font-size: 0; padding: 12px 0; }

/* More breathing room between the icons now that labels aren't spacing them. */
.sidebar.is-mini .nav-items { gap: 8px; }
.sidebar.is-mini .nav-group + .nav-group { margin-top: 12px; }

/* In icon mode, show every group's icons even if it was collapsed,
   so no section silently disappears. */
.sidebar.is-mini .nav-group.is-collapsed .nav-items { display: flex; }

/* Push the main content over to meet the narrow sidebar. */
.sidebar.is-mini ~ .main { margin-left: var(--sidebar-mini-w); }

/* Each section is hidden by default; app.js adds .is-visible to
   the active one. The fade-in gives a soft, premium feel. */
/* THE READING WIDTH LIVES HERE, and it centres itself.

   1400 rather than 1100 because of what it does to the grid: cards are
   minmax(260px, 1fr) with a 20px gap, so 1400 - 112px of padding gives
   four columns instead of three. "Today's focus" holds exactly four
   cards, so it becomes one clean row instead of three-plus-an-orphan.

   `margin: 0 auto` is the half that fixes the lopsidedness: any width
   beyond 1400 is split evenly instead of all landing on the right.
   Capped rather than fluid so text in the settings forms and the weight
   tab never runs to an unreadable line length on a very wide monitor. */
.view { display: none; max-width: 1400px; margin: 0 auto; }
.view.is-visible {
  display: block;
  animation: fade .4s ease;
}
@keyframes fade {
  from { opacity: 0; transform: translateY(8px); }
  to   { opacity: 1; transform: translateY(0); }
}

.view-head { margin-bottom: 32px; }
.view-head h1 {
  font-size: 40px;
  font-weight: 700;
  letter-spacing: -0.02em;
}
.view-sub {
  margin-top: 6px;
  font-size: 17px;
  color: var(--ink-soft);
}

/* ============================================================
   HOMEPAGE
   ============================================================ */
.home-hero {
  display: flex;
  align-items: center;
  gap: 20px;
  margin-bottom: 8px;
  /* Apple-style card: surface, hairline, rounded, soft shadow */
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  box-shadow: var(--shadow);
  padding: 38px 32px;
  /* NARROWER, AND STILL LEFT-ALIGNED.

     The card was taking the full 1288px content width to hold a photo,
     two lines of text and a button — so most of it was empty. `max-width`
     caps it; there is deliberately NO `margin: 0 auto`, because centring
     it is the other possible fix and not the one asked for: it stays
     against the left edge, in line with everything that will sit under
     it later.

     680px. 640 was tight, 760 a little loose. This is roughly what — avatar, name,
     the contents occupy — avatar, name, role, and the View Profile
     button pushed right by its own margin-left:auto. Much narrower
     and that button crowds the name. */
  max-width: 680px;
}
/* soft drop shadow so the photo feels lifted/alive */
.home-hero .avatar-lg { box-shadow: 0 6px 18px rgba(0, 0, 0, .22); }

/* "View Profile" button — pushed to the right edge of the hero card */
.hero-action {
  margin-left: auto;          /* eats the free space, shoving it right */
  display: inline-flex;
  align-items: center;
  gap: 3px;
  flex-shrink: 0;
  text-decoration: none;      /* it's an <a> now — no underline */
  padding: 9px 15px;
  border: none;               /* no outline — one cue only (the soft fill) */
  border-radius: 999px;
  background: var(--bg);
  color: var(--ink);          /* readable, not faint */
  font: inherit;
  font-size: 14px;
  cursor: pointer;
  transition: background .15s ease;
}
.hero-action:hover { background: var(--line); }   /* fill darkens a touch on hover */
.hero-action svg { width: 14px; height: 14px; }

.home-role { color: var(--ink-soft); font-weight: 400; }

/* the role line under the name */
.home-subtitle {
  display: flex;
  align-items: center;
  gap: 5px;
  flex-wrap: wrap;
  margin-top: 5px;
  font-size: 15px;
  line-height: 1.2;
  color: var(--ink-soft);
}
.home-title {
  font-size: 22px;
  font-weight: 400;
  letter-spacing: -0.01em;
  line-height: 1.2;
  margin-top: 0;
}

.section-label {
  font-size: 13px;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--ink-soft);
  margin: 36px 0 16px;
}

/* The "Today's focus" row gets a touch more presence */
.grid-focus { grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); }

/* A card that acts like a link to another section */
.card-jump { cursor: pointer; }
.jump-icon {
  display: block;
  width: 24px;
  height: 24px;
  margin-bottom: 12px;
  color: var(--accent);   /* the line icon inherits this colour */
}

/* ============================================================
   CARDS  —  responsive grid that reflows on smaller screens
   ============================================================ */
.grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
  gap: 20px;
}

.card {
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  padding: 24px;
  box-shadow: var(--shadow);
  transition: transform .2s ease, box-shadow .2s ease;
}
.card:hover {
  transform: translateY(-3px);
  box-shadow: 0 4px 12px rgba(0,0,0,.06), 0 16px 40px rgba(0,0,0,.08);
}
.card h3 { font-size: 18px; font-weight: 600; letter-spacing: -0.01em; }

/* Small pill to mark something as not-yet-real, e.g. "future" */
.tag {
  display: inline-block;
  margin-left: 6px;
  padding: 2px 8px;
  border-radius: 999px;
  background: var(--bg);
  color: var(--ink-soft);
  font-size: 11px;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  vertical-align: middle;
}
.muted { margin-top: 8px; font-size: 14px; color: var(--ink-soft); }

.card-feature { border-color: var(--accent); }
.card-link {
  display: inline-block;
  margin-top: 14px;
  font-size: 14px;
  font-weight: 500;
  color: var(--accent);
  text-decoration: none;
}
.card-link:hover { text-decoration: underline; }

/* ============================================================
   RESPONSIVE  —  collapse the sidebar into a top bar on mobile
   ============================================================ */
@media (max-width: 720px) {
  .sidebar {
    position: static;
    top: auto; left: auto; bottom: auto;
    width: 100%;
    border-right: none;
    border-bottom: 1px solid var(--line);
    flex-direction: column;
  }
  .nav { flex-direction: row; flex-wrap: wrap; }
  .nav-label { width: 100%; }
  .profile-chip { padding-bottom: 10px; }
  .sidebar-toggle { display: none; }   /* edge button makes no sense as a top bar */
  .main { margin-left: 0; padding: 32px 20px 60px; }
  .view-head h1 { font-size: 32px; }
}

/* ============================================================
   PROFILE PAGE (profile.html) — a standalone, LinkedIn-style page
   ============================================================ */
.profile-topbar {
  max-width: 1100px;
  margin: 0 auto;
  padding: 28px 56px 0;
}
.back-link {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  color: var(--ink-soft);
  text-decoration: none;
  font-size: 14px;
  transition: color .15s ease;
}
.back-link:hover { color: var(--ink); }
.back-link svg { width: 16px; height: 16px; }

/* Matches the dashboard's .main exactly — same 1100px cap, same 56px
   gutters — so the two pages read as one site. It stays CENTRED rather than
   left-aligned because this page has no sidebar to sit beside; left-aligning
   it would leave a 260px empty gutter for no reason. */
.profile-main {
  max-width: 1100px;
  margin: 0 auto;
  padding: 18px 56px 80px;
  display: flex;
  flex-direction: column;
  gap: 20px;
}
/* these cards aren't clickable, so cancel the hover-lift from .card */
.profile-main .card:hover { transform: none; box-shadow: var(--shadow); }

/* header card: a clean flat banner (no cover) with avatar + name + affiliation,
   and a tab bar along the bottom */
.profile-header { padding: 0; }
.profile-header-body {
  display: flex;
  align-items: center;
  gap: 22px;
  padding: 28px 30px;
}
.profile-photo {
  display: block;
  width: 84px;
  height: 84px;
  border-radius: 50%;
  overflow: hidden;
  flex-shrink: 0;
  background: var(--surface);
  box-shadow: 0 0 0 1px var(--line);   /* subtle ring, like the reference */
}
/* The scale(1.65) that used to live here is now in the
   :root:not(.has-custom-photo) block above, for the reason explained there. */
.profile-photo img {
  width: 100%;
  height: 100%;
  object-fit: cover;
}
.profile-id h1 { font-size: 26px; font-weight: 600; letter-spacing: -0.01em; }
.profile-affil { margin-top: 5px; color: var(--ink-soft); font-size: 15px; }

/* tab bar along the bottom of the banner card */
.profile-tabs { display: flex; gap: 6px; padding: 0 18px; }
.profile-tab {
  background: none;
  border: none;
  font: inherit;
  font-size: 14px;
  color: var(--ink-soft);
  padding: 14px;
  cursor: pointer;
  border-bottom: 2px solid transparent;   /* space for the active underline */
  transition: color .15s ease;
}
.profile-tab:hover { color: var(--ink); }
.profile-tab.is-active { color: var(--ink); border-bottom-color: var(--accent); }

/* tab panels — only the active one shows; a panel can hold several cards */
.profile-panel { display: none; }
.profile-panel.is-active { display: flex; flex-direction: column; gap: 20px; animation: fade .3s ease; }

/* two cards side by side (Body + Contact) */
.profile-row { display: flex; gap: 20px; align-items: flex-start; }
.profile-row > .card { flex: 1; min-width: 0; }
@media (max-width: 640px) { .profile-row { flex-direction: column; } }

.profile-section h2 { font-size: 18px; font-weight: 600; margin-bottom: 16px; }
.profile-section p { color: var(--ink-soft); line-height: 1.65; }

/* ===== Timeline (Education, Career, Football, Places) ===== */
.timeline { list-style: none; position: relative; margin: 0; padding: 4px 0; }
/* the vertical line running down the dots */
.timeline::before {
  content: "";
  position: absolute;
  left: 6px;
  top: 10px;
  bottom: 10px;
  width: 2px;
  background: var(--line);
}
.timeline-item { position: relative; padding-left: 30px; padding-bottom: 26px; }
.timeline-item:last-child { padding-bottom: 0; }
/* the dot — a ring of the card colour makes it sit cleanly on the line */
.timeline-item::before {
  content: "";
  position: absolute;
  left: 1px;
  top: 5px;
  width: 12px;
  height: 12px;
  border-radius: 50%;
  background: var(--accent);
  box-shadow: 0 0 0 3px var(--surface);
}
.timeline-head {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  gap: 12px;
  flex-wrap: wrap;
}
.timeline-title { font-size: 16px; font-weight: 600; letter-spacing: -0.01em; }
.timeline-date { font-size: 13px; color: var(--ink-soft); white-space: nowrap; }
/* A date that also carries a duration stacks the two, right-aligned — same
   idea as Career's .career-when, just under a different name since nothing
   here computes the duration the way career.js does. */
.timeline-when { display: flex; flex-direction: column; align-items: flex-end; gap: 2px; }
.timeline-dur { font-size: 12px; color: var(--ink-soft); white-space: nowrap; }
.timeline-sub { font-size: 14px; color: var(--ink-soft); margin-top: 3px; }
/* Title + subtitle as one column, exactly like .career-id / .pos-id: their
   own gap sets the spacing between them, so it can never depend on how tall
   the date column on the right happens to be. */
.timeline-id { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 3px; }
.timeline-id .timeline-sub { margin-top: 0; }   /* the flex gap already spaces them */
.timeline-desc { font-size: 14px; color: var(--ink); line-height: 1.6; margin-top: 8px; }

/* small level/type badge (Degree, Secondary, etc.) */
.level-tag {
  display: inline-block;
  margin-right: 7px;
  padding: 2px 9px;
  border-radius: 999px;
  background: var(--accent-soft);
  color: var(--accent);
  font-size: 11px;
  font-weight: 500;
}

/* structured detail rows: "Label   value" with labels aligned in a column */
/* Was a single stacked column, which on a 1100px card meant a tall thin
   list of 16 rows with the right half of the card empty. Now the rows flow
   into as many columns as fit. */
.timeline-detail {
  margin-top: 10px;
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
  gap: 6px 30px;
}
.timeline-detail > div { display: flex; gap: 10px; font-size: 14px; line-height: 1.5; }
.timeline-detail dt { flex-shrink: 0; width: 110px; color: var(--ink-soft); }
.timeline-detail dd { margin: 0; color: var(--ink); }

/* entry with a logo on the left */
.timeline-entry { display: flex; gap: 14px; }
/* Same icon-with-optional-logo slot as Career's .career-logo: position:relative
   lets the <img> layer on top of the icon rather than sit beside it as a
   second flex item, which is what would happen without it. Unused until now —
   no education entry has ever included an <img>, only the Lucide icon. */
.timeline-logo {
  position: relative;
  flex-shrink: 0;
  width: 44px;
  height: 44px;
  border-radius: 10px;
  background: var(--bg);
  display: flex;
  align-items: center;
  justify-content: center;
  color: var(--ink-soft);
  overflow: hidden;
}
/* contain, not cover: a crest or logo must never be cropped. White behind it
   (via var(--bg) above) so a transparent-background logo still reads
   correctly in dark mode — same reasoning as .career-logo. */
.timeline-logo img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: contain; }
.timeline-logo svg { width: 22px; height: 22px; }
/* Overrides the fixed 44px height with a stretch to match .timeline-id (the
   title+subtitle column) beside it — .timeline-entry's default flex
   align-items is already "stretch", so removing the fixed height is all this
   takes. Combined selector for specificity, so it wins regardless of source
   order against the plain .timeline-logo rule above. */
.timeline-logo.timeline-logo--fit-head { height: auto; align-self: stretch; }
.timeline-body { flex: 1; min-width: 0; }

/* A summary + description beside one photo, under a rule — "a line, text on
   the left, an image on the right." The border-top IS the line: matches how
   .career-sub separates a nested block elsewhere, rather than a bare <hr>.
   (An expand/collapse toggle sat here briefly — tried and dropped; always
   visible again.) */
.edu-feature {
  display: flex;
  gap: 20px;
  align-items: flex-start;
  margin-top: 14px;
  padding-top: 14px;
  border-top: 1px solid var(--line);
}
/* Capped, not flex:1 — letting the text fill "whatever's left beside a 160px
   photo" gave two short sentences a very wide, mostly-empty box, which reads
   badly even once real text goes in. 520px keeps its line length readable
   regardless of how much card width is actually available. */
.edu-feature-text { flex: 0 1 520px; min-width: 0; display: flex; flex-direction: column; gap: 8px; }
.edu-feature-summary { font-size: 14px; font-weight: 600; color: var(--ink); }
.edu-feature-desc { font-size: 14px; color: var(--ink-soft); line-height: 1.6; }
/* Activities/Achievements-style facts, when they live inside the feature text
   (below the description) rather than a header disclosure — ONLY the label
   is bolder and full-ink; the value stays regular weight, same as
   .timeline-detail's own default everywhere else, so "Football club — Ahli
   Biasa" doesn't read as shouted just because its label does. Scoped to
   .edu-feature-text only: .timeline-detail's own dt/dd styling is shared by
   Result/Honours/Stream/UPSR/UPKK across every other card, and this shouldn't
   bold all of those too. */
.edu-feature-text .timeline-detail dt,
.edu-feature-detail dt { font-weight: 600; color: var(--ink); }
/* List-style facts (Leadership/Sport/Activities as bullets, not one run-on
   sentence) reuse .pos-points — the exact bullet style Career already uses
   for "what you did" and Achievements. Its own margin-top:12px assumes it
   follows a paragraph; as a dd's ONLY content it would push the list down
   below where dt's label sits, so it's zeroed when nested directly here. */
.edu-feature-detail dd .pos-points { margin-top: 0; }
/* .timeline-detail's default puts dt in a FIXED 110px column beside dd — sized
   for a short value like a date or a grade, not a heading over a list. Since
   "Leadership" (~75px) doesn't fill that column, the list started roughly
   110px + the 10px gap from the label — most of it dead space, which is what
   read as "too far." Stacking the label directly above its list (a heading
   over content, not a label beside a value) closes that gap entirely rather
   than just shrinking a number. Combined selectors, same reason as
   .timeline-logo--fit-head: equal specificity against the base rules above,
   so this wins on specificity rather than depending on staying below them. */
.timeline-detail.edu-feature-detail > div { flex-direction: column; gap: 4px; }
.timeline-detail.edu-feature-detail dt { width: auto; }
/* Reuses .media-frame (the rail's own icon/img-with-fallback tile) solo — one
   photo has no need for the rail's scroll/snap machinery. flex:1 lets it
   claim whatever space the capped text column leaves behind, up to 340px —
   a real feature photo now (340×255 at 4:3) instead of a 160×120 thumbnail
   that read as an afterthought next to the text. */
.edu-feature-media { flex: 1; min-width: 200px; max-width: 340px; }
@media (max-width: 560px) {
  .edu-feature { flex-direction: column; }
  .edu-feature-text { flex-basis: auto; max-width: none; }
  .edu-feature-media { max-width: none; width: 100%; }
}

/* ---- .edu-feature--stacked: photos on top, text below, introduced with
   SK Tanjong Mas ----
   A MODIFIER on .edu-feature, not a replacement — Kindergarten and Sungai
   Isap still use the plain side-by-side rules above, untouched. Combined
   selector for specificity, so it overrides the base rule's row layout
   regardless of source order (same technique as .timeline-logo--fit-head).
   align-items: stretch (not the base rule's flex-start) is the part that
   actually matters here: in a flex COLUMN, flex-start would let the rail,
   the paragraphs and the grid each shrink to their own content width
   instead of filling the card — exactly wrong for a "full width" layout. */
.edu-feature.edu-feature--stacked {
  flex-direction: column;
  align-items: stretch;
  gap: 14px;
}
/* Leadership/Activities/Achievements forced into exactly 2 columns.
   .timeline-detail's own default is auto-fit at a 300px minimum, which at
   full card width fits 3 columns, not the requested "2 per row, third one
   wraps to a new row under the first." Combined selector, same reason as
   above. */
.timeline-detail.edu-feature-detail { grid-template-columns: repeat(2, minmax(0, 1fr)); }
@media (max-width: 560px) {
  .timeline-detail.edu-feature-detail { grid-template-columns: 1fr; }
}

/* expandable "Show more" details */
.timeline-more { display: none; }
.timeline-more.is-open { display: block; }
.timeline-toggle {
  display: inline-block;
  text-align: left;
  margin-top: 10px;
  background: none;
  border: none;
  padding: 0;
  font: inherit;
  font-size: 13px;
  color: var(--ink-soft);
  cursor: pointer;
  transition: color .15s ease;
}
.timeline-toggle:hover { color: var(--ink); }
/* Career, Courses and Events want their See more CENTERED under the
   summary/description, not flush-left like every other toggle on the page
   (Education, Sports, etc. keep the base rule above, untouched).
   display:block + width:fit-content + auto side margins centers the button
   itself directly — deliberate and self-contained, rather than depending
   on a parent's text-align, which is what made this drift on its own
   before and needed pinning down explicitly either way. */
.pos-summary .timeline-toggle,
#courses-list .timeline-toggle,
#events-list .timeline-toggle,
#projects-list .timeline-toggle {
  display: block;
  width: fit-content;
  margin: 10px auto 0;
}

/* Expandable About text — collapsed by default with a fade, "Read more" expands it */
.about-text {
  position: relative;
  overflow: hidden;
  max-height: 11.5em;                /* collapsed height (teaser) */
  transition: max-height .35s ease;
}
.about-text.is-expanded { max-height: 200em; }   /* big enough to show everything */
.about-text p + p { margin-top: 14px; }
/* Fills the full card width — Izzat's call, and the reason the collapsed
   height below is generous: wider lines mean fewer of them. */
.about-text p { line-height: 1.7; color: var(--ink); }
.about-lead { font-size: 17px; color: var(--ink); }   /* the opening line stands out */
/* fade the bottom into the card while collapsed, hinting there's more */
.about-text:not(.is-expanded)::after {
  content: "";
  position: absolute;
  left: 0; right: 0; bottom: 0;
  height: 2em;
  background: linear-gradient(transparent, var(--surface));
  pointer-events: none;
}
.read-more-btn {
  display: block;
  width: fit-content;
  margin: 6px auto 0;        /* small gap above, centered (auto left+right) */
  background: none;
  border: none;
  padding: 0;
  font: inherit;
  font-size: 13px;
  color: var(--ink-soft);    /* muted grey — understated */
  cursor: pointer;
  transition: color .15s ease;
}
.read-more-btn:hover { color: var(--ink); }   /* darkens on hover */

/* Detailed label → value grid (Identity, Body, Origins, Contact) */
/* auto-fit, not a fixed 2 columns: the card is 1100px wide now, and two
   half-width columns holding "Male" and "Islam" left most of the row empty.
   auto-fit packs in as many 260px columns as fit — 3 on desktop, 2 on a
   tablet, 1 on a phone — with no media queries doing the work. */
.info-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
  gap: 18px 36px;
}
.info-grid.one-col { grid-template-columns: 1fr; }   /* single column for the narrow side-by-side cards */
.info-row { display: flex; flex-direction: column; gap: 3px; }
.info-row dt { font-size: 12px; color: var(--ink-soft); }   /* the label */
.info-row dd { font-size: 15px; color: var(--ink); margin: 0; } /* the value */
.info-row dd a { color: inherit; text-decoration: none; }  /* blend with text, not blue */
.info-row dd a:hover { text-decoration: underline; }       /* underline on hover hints it's clickable */
.info-empty { color: var(--ink-soft); }   /* placeholder dash for unfilled fields */

/* value cell with a flag image (e.g. nationality) */
.dd-flag { display: flex; align-items: center; gap: 8px; }
.flag {
  height: 12px;
  width: auto;
  border-radius: 2px;
  box-shadow: 0 0 0 1px var(--line);   /* subtle border so light flags don't blend */
}

@media (max-width: 600px) { .info-grid { grid-template-columns: 1fr; } }

/* ============================================================
   SETTINGS SECTION  (#settings)
   Site-wide preferences. Prefixed "set-" for the same reason the
   tracker uses "wt-" — this stylesheet is shared across pages.
   ============================================================ */
.set-card { padding: 0; overflow: hidden; }
.set-card:hover { transform: none; box-shadow: var(--shadow); }
.set-card + .set-card { margin-top: 18px; }

/* Only one settings pane is on screen at a time. */
.set-pane { display: none; }
.set-pane.is-active { display: block; animation: fade .3s ease; }

/* A drill-in row: tap it to open a pane. */
.set-menu-row {
  display: flex;
  align-items: center;
  gap: 14px;
  width: 100%;
  padding: 18px 22px;
  border: none;
  background: transparent;
  font: inherit;
  text-align: left;
  cursor: pointer;
  transition: background .15s ease;
}
.set-menu-row:hover { background: var(--bg); }
.set-menu-avatar {
  width: 44px; height: 44px;
  border-radius: 50%;
  object-fit: cover;
  flex-shrink: 0;
  background: var(--bg);
  box-shadow: 0 0 0 1px var(--line);
}
.set-menu-text { display: flex; flex-direction: column; gap: 2px; flex: 1; min-width: 0; }
.set-menu-label { font-size: 16px; color: var(--ink); }
.set-menu-desc {
  font-size: 13px;
  color: var(--ink-soft);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.set-menu-chevron { width: 18px; height: 18px; color: var(--ink-soft); flex-shrink: 0; }
/* Same 44px slot as .set-menu-avatar, so rows with an icon and rows with a
   photo line their text up on the same left edge. */
.set-menu-icon {
  width: 44px; height: 44px;
  border-radius: 50%;
  flex-shrink: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  background: var(--bg);
  box-shadow: 0 0 0 1px var(--line);
}
.set-menu-icon svg { width: 20px; height: 20px; color: var(--ink-soft); }
/* Two drill-in rows in one card need a line between them, but not above the
   first one — otherwise it doubles up with the card's own top edge. */
.set-menu-row + .set-menu-row { border-top: 1px solid var(--line); }

/* Back out of a pane to the menu. */
.set-back {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  margin-bottom: 16px;
  padding: 7px 14px 7px 8px;
  border: 1px solid var(--line);
  border-radius: 980px;
  background: var(--surface);
  font: inherit;
  font-size: 14px;
  color: var(--ink);
  cursor: pointer;
  transition: background .15s ease;
}
.set-back:hover { background: var(--bg); }
.set-back svg { width: 16px; height: 16px; }

.set-group-title {
  font-size: 12px;
  font-weight: 400;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--ink-soft);
  padding: 18px 24px 10px;
}
.set-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 24px;
  padding: 16px 24px 20px;
  border-top: 1px solid var(--line);
}
.set-row-text { display: flex; flex-direction: column; gap: 3px; min-width: 0; }
.set-row-label { font-size: 16px; color: var(--ink); }
.set-row-desc { font-size: 13px; color: var(--ink-soft); }

/* iOS-style switch. The real <input type="checkbox"> is stretched over
   the whole control at opacity 0 — so every click, tap, keypress and
   screen-reader announcement is handled natively, and the visible
   slider is just decoration driven by :checked. */
.set-switch {
  position: relative;
  display: inline-block;
  width: 51px;
  height: 31px;
  flex-shrink: 0;
}
.set-switch input {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  margin: 0;
  opacity: 0;
  cursor: pointer;
  z-index: 2;
}
/* Visually hidden text label, so the switch is not an unlabelled control
   for anyone using a screen reader. */
.set-switch-label {
  position: absolute;
  width: 1px; height: 1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}
.set-slider {
  position: absolute;
  inset: 0;
  border-radius: 999px;
  background: var(--line);
  transition: background .22s ease;
}
.set-slider::before {
  content: "";
  position: absolute;
  top: 2px; left: 2px;
  width: 27px; height: 27px;
  border-radius: 50%;
  background: #fff;
  box-shadow: 0 1px 3px rgba(0, 0, 0, .25);
  transition: transform .22s cubic-bezier(.4, 0, .2, 1);
}
.set-switch input:checked ~ .set-slider { background: var(--accent); }
.set-switch input:checked ~ .set-slider::before { transform: translateX(20px); }
.set-switch input:focus-visible ~ .set-slider {
  outline: 2px solid var(--accent);
  outline-offset: 3px;
}

/* ---- the schema-built profile form ---- */
.set-fieldset { border-top: 1px solid var(--line); padding: 16px 24px 20px; }
.set-fieldset-title {
  font-size: 13px;
  font-weight: 500;
  color: var(--accent);
  margin-bottom: 14px;
}
.set-field-grid {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 16px 24px;
}
.set-field-grid.is-single { grid-template-columns: 1fr; }
.set-field-grid + .set-field-grid { margin-top: 16px; }
.set-field { display: flex; flex-direction: column; gap: 6px; min-width: 0; }
.set-field-label { font-size: 13px; color: var(--ink-soft); }
.set-hint { font-size: 12px; color: var(--ink-soft); }
.set-hint code {
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: 11px;
  background: var(--bg);
  border: 1px solid var(--line);
  border-radius: 4px;
  padding: 1px 4px;
}
/* A short explanation under a card's title, where the card needs one line of
   "what belongs here" before you start typing. */
.set-card-note {
  font-size: 13px;
  color: var(--ink-soft);
  padding: 0 24px 16px;
  max-width: 62ch;
}

/* ---- the career job list ----
   One row per company. Deliberately NOT .set-menu-row: those open a pane and
   nothing else, while these carry three controls, so a click anywhere on the
   row would be ambiguous. The buttons are the only targets. */
.car-row {
  display: flex;
  align-items: center;
  gap: 14px;
  padding: 14px 24px;
  border-top: 1px solid var(--line);
}
.car-row-text { display: flex; flex-direction: column; gap: 2px; flex: 1; min-width: 0; }
.car-row-title { font-size: 15px; font-weight: 500; color: var(--ink); }
.car-row-sub {
  font-size: 13px;
  color: var(--ink);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.car-row-meta { font-size: 12px; color: var(--ink-soft); }
.car-row-btns { display: flex; gap: 6px; flex-shrink: 0; }
/* "Remove this role" sits inside a fieldset title, so it has to read as a
   quiet link rather than compete with the heading it sits beside. Reused for
   "+ Add row" / "+ Add photo" — same weight, same reason. */
.car-mini {
  border: none;
  background: none;
  font: inherit;
  font-size: 12px;
  color: var(--ink-soft);
  cursor: pointer;
  padding: 0 0 0 8px;
  text-decoration: underline;
}
.car-mini:hover { color: var(--ink); }

@media (max-width: 560px) {
  /* The Edit button would squeeze the company name to nothing. */
  .car-row { flex-wrap: wrap; }
  .car-row-btns { width: 100%; }
}

/* A <select> plus a "Custom…" escape hatch (employment type, work mode,
   status, and each row's type) share this one input for the free-text
   fallback, toggled by the generic [data-car-toggle] listener.
   .car-row-type and .car-row-href live OUTSIDE a .set-field label (the row
   editor's own layout needs them side by side with the link checkbox), so
   they need their own base styling rather than inheriting .set-field's. */
.car-custom-input,
.car-row-type,
.car-row-href {
  font: inherit;
  font-size: 14px;
  color: var(--ink);
  background: var(--bg);
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  padding: 8px 10px;
  width: 100%;
}
.car-custom-input { margin-top: 6px; }
.car-custom-input:focus, .car-row-type:focus, .car-row-href:focus {
  outline: none;
  border-color: var(--accent);
}

/* ---- company logo ---- */
.car-logo-row { border-top: none; }
.car-logo-preview {
  width: 60px; height: 60px;
  border-radius: var(--radius-sm);    /* square-ish, not round — logos aren't faces */
  display: flex;
  align-items: center;
  justify-content: center;
  background: var(--bg);
  box-shadow: 0 0 0 1px var(--line);
  overflow: hidden;
  flex-shrink: 0;
}
.car-logo-preview img { width: 100%; height: 100%; object-fit: contain; }
.car-logo-preview svg { width: 24px; height: 24px; color: var(--ink-soft); }

/* ---- Rows and Photos sub-sections, inside a role's fieldset ---- */
.car-rows-block, .car-media-block { margin-top: 18px; }
.car-block-title {
  font-size: 12px;
  font-weight: 500;
  text-transform: uppercase;
  letter-spacing: 0.05em;
  color: var(--ink-soft);
  margin-bottom: 10px;
}

/* One row: a type select, an optional link toggle, and its tags. */
.car-rowedit {
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  padding: 12px 14px;
  margin-bottom: 10px;
}
.car-rowedit-head {
  display: flex;
  align-items: center;
  gap: 12px;
  flex-wrap: wrap;
}
.car-rowedit-head select { width: auto; min-width: 160px; flex: 1; }
.car-rowedit-head .car-custom-input { width: auto; flex: 1; margin-top: 0; }
.car-row-link {
  display: flex;
  align-items: center;
  gap: 6px;
  font-size: 13px;
  color: var(--ink-soft);
  white-space: nowrap;
  cursor: pointer;
}
.car-row-href { margin-top: 8px; }

/* Chips: existing items, then the input that adds the next one — same row,
   so typing the next tag feels like continuing a sentence. */
.car-tags {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 6px;
  margin-top: 10px;
  padding: 6px 8px;
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  background: var(--bg);
}
.car-tags:focus-within { border-color: var(--accent); }
.car-tag {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  background: var(--bg);
  border: 1px solid var(--line);
  border-radius: 980px;
  padding: 4px 6px 4px 12px;
  font-size: 13px;
  color: var(--ink);
}
.car-tag-x {
  border: none;
  background: none;
  color: var(--ink-soft);
  font-size: 14px;
  line-height: 1;
  width: 18px; height: 18px;
  border-radius: 50%;
  cursor: pointer;
}
.car-tag-x:hover { background: var(--line); color: var(--ink); }
.car-tag-input {
  flex: 1;
  min-width: 140px;
  border: none;
  background: none;
  font: inherit;
  font-size: 13px;
  color: var(--ink);
  padding: 4px 0;
}
.car-tag-input:focus { outline: none; }

/* ---- photos ---- */
.car-media-list { display: flex; flex-direction: column; gap: 8px; }
.car-media-item {
  display: flex;
  align-items: center;
  gap: 10px;
}
.car-media-thumb {
  width: 44px; height: 44px;
  border-radius: var(--radius-sm);
  display: flex;
  align-items: center;
  justify-content: center;
  background: var(--bg);
  box-shadow: 0 0 0 1px var(--line);
  overflow: hidden;
  flex-shrink: 0;
}
.car-media-thumb img { width: 100%; height: 100%; object-fit: cover; }
.car-media-thumb svg { width: 18px; height: 18px; color: var(--ink-soft); }
.car-media-alt { flex: 1; min-width: 0; }
.car-media-item .wt-btn { flex-shrink: 0; }

.set-field input, .set-field select, .set-field textarea {
  font: inherit;
  font-size: 15px;
  color: var(--ink);
  background: var(--bg);
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  padding: 9px 12px;
  width: 100%;
  transition: border-color .15s ease;
}
.set-field textarea { resize: vertical; line-height: 1.5; font-size: 14px; }
.set-field input:focus, .set-field select:focus, .set-field textarea:focus {
  outline: none;
  border-color: var(--accent);
}
.set-input-wrap { position: relative; display: block; }
.set-input-wrap input { padding-right: 46px; }
.set-input-unit {
  position: absolute;
  right: 12px; top: 50%;
  transform: translateY(-50%);
  font-size: 13px;
  color: var(--ink-soft);
  pointer-events: none;
}

/* ---- photo picker ---- */
.set-row-photo { border-top: none; }
.set-photo-picker { display: flex; align-items: center; gap: 14px; flex-shrink: 0; }
.set-photo-preview {
  width: 60px; height: 60px;
  border-radius: 50%;
  object-fit: cover;                  /* fills the circle without squashing */
  background: var(--bg);
  box-shadow: 0 0 0 1px var(--line);
}
.set-photo-btns { display: flex; flex-direction: column; gap: 6px; }

.set-row-actions { align-items: center; flex-wrap: wrap; }
.set-view-link {
  font-size: 14px;
  color: var(--accent);
  text-decoration: none;
}
.set-view-link:hover { text-decoration: underline; }

/* ---- photo adjuster ---- */
.id-crop-body { padding: 20px 22px 24px; display: flex; flex-direction: column; gap: 16px; align-items: center; }
.id-crop-stage {
  position: relative;
  width: 272px; height: 272px;
  border-radius: var(--radius-sm);
  overflow: hidden;                    /* clips the mask's huge shadow */
  background: #1a1a1a;                 /* dark behind the photo, both themes */
  touch-action: none;                  /* let us handle the drag, not the browser */
  cursor: grab;
}
.id-crop-stage:active { cursor: grabbing; }
.id-crop-stage canvas { display: block; }
/* A circle with an enormous spread shadow = everything OUTSIDE the circle
   goes dark. Cheaper and sharper than an SVG mask. */
.id-crop-mask {
  position: absolute;
  inset: 0;
  border-radius: 50%;
  box-shadow: 0 0 0 999px rgba(0, 0, 0, .55);
  border: 2px solid rgba(255, 255, 255, .9);
  pointer-events: none;                /* never blocks the drag */
}
.id-crop-zoom { display: flex; align-items: center; gap: 12px; width: 100%; }
.id-crop-zoom input[type="range"] { flex: 1; accent-color: var(--accent); }
.id-crop-hint { font-size: 12px; text-align: center; }

/* The "borrowed from your profile" note in the Weight tab */
.wt-borrowed {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 16px;
  flex-wrap: wrap;
  padding: 12px 14px;
  border-radius: var(--radius-sm);
  background: var(--accent-soft);
}
.wt-borrowed-text { font-size: 13px; color: var(--ink); margin: 0; }

@media (max-width: 700px) {
  .set-field-grid { grid-template-columns: 1fr; }
}
@media (max-width: 600px) {
  .set-row { gap: 14px; flex-direction: column; align-items: flex-start; }
  .set-row-photo { flex-direction: row; align-items: center; }
}

/* ============================================================
   WEIGHT TRACKER  (section #weight + weight.js)
   ------------------------------------------------------------
   Every class here is prefixed "wt-". That is deliberate: this
   stylesheet is shared with profile.html, and a generic name like
   ".stat" or ".meter" WILL eventually collide with something else
   and silently break a layout somewhere you are not looking.
   (That already happened on this site once — .profile-meta in the
   sidebar vs the profile page. A prefix costs nothing, prevents it.)
   ============================================================ */

/* ---- tab bar ---- */
/* profile.html topbar now holds two links (back + edit) */
.profile-topbar { display: flex; align-items: center; gap: 20px; flex-wrap: wrap; }

/* ============================================================
   PROFILE PAGE — Skills, credentials, languages, GPA breakdown
   ============================================================ */

/* Six tabs no longer fit on a narrow screen, so let the bar scroll
   sideways instead of squashing or wrapping the labels. */
.profile-tabs { overflow-x: auto; scrollbar-width: none; }
.profile-tabs::-webkit-scrollbar { display: none; }

/* ---- skills ---- */
.skills-note { font-size: 13px; margin-bottom: 18px; }
/* A note sitting DIRECTLY under a heading is a subtitle, not a paragraph.
   It belongs to the heading, so it should sit close to it — the site-wide
   16px under an h2 is there to separate a heading from unrelated content
   below, and at that distance a subtitle floats loose and reads as its own
   thing.

   :has() is what makes this scopeable: CSS can't select a PREVIOUS sibling,
   so there's no way to say "the note pulls the heading down." :has() flips
   it round — the heading reacts to what follows it. Every other h2 on the
   site keeps its full 16px, untouched. And in a browser without :has() the
   rule is simply ignored and the old gap stays, so the worst case is the
   spacing we already had, never a broken card. */
.profile-section h2:has(+ .skills-note) { margin-bottom: 3px; }
/* This tab's cards sit two-to-a-row, not one card per full width, so their
   headings don't need to carry a whole row on their own — the site-wide
   18px was sized for a heading that does. Scoped to the tab's container so
   About, Body, Contact and every other tab keep 18px untouched, and set on
   the container rather than the four card classes so the Investment /
   Software / Entrepreneurship pages match instead of jumping a size when
   the switcher changes page. */
#skills-list h2 { font-size: 16px; }
.skills-note code {
  font-size: 12px;
  background: var(--bg);
  padding: 1px 5px;
  border-radius: 4px;
}
.skill-group + .skill-group { margin-top: 20px; }
/* SELECTOR NEEDS `.card` TO WIN. `.card h3` sets 18px/600 and outranks a
   bare `.skill-cat` — one class beats none against the element part — so
   the 13px below was being ignored and these rendered as big blue
   headings. Matching `.card h3.skill-cat` takes the specificity back.

   Styled like FotMob's "Shooting" / "Passing": a quiet label that separates
   groups, in the same ink as the rows. It was the ACCENT colour, which
   made two section names the loudest thing on a card whose subject is the
   skills under them. */
.card h3.skill-cat {
  font-size: 13px;
  font-weight: 600;
  color: var(--ink);
  margin: 0 0 4px;
  letter-spacing: -0.01em;
}
/* Air above each section after the first, since the rows no longer carry
   dividers to separate them. */
.skill-top .skill-toplist + .skill-cat { margin-top: 18px; }
.skill-list {
  list-style: none;
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
}
/* A chip, not a bullet: skills are a set, not a sequence. */
.skill {
  font-size: 14px;
  padding: 6px 13px;
  border-radius: 980px;
  background: var(--bg);
  border: 1px solid var(--line);
  color: var(--ink);
}

/* ---- a skill and its provenance ----
   The chip above still exists — it's what the Top skills strip is made
   of, where a name with no evidence under it is exactly right. But a
   skill in the main list carries its sources, and a chip has nowhere to
   put them. So it grows into a small card.

   Two across, not three: a source line is a logo plus two lines of text,
   and three columns squeezed those onto a second line each. */
/* ---- the Skills tab's own stack ----
   `.profile-panel.is-active` is a flex column with gap: 20px, and that gap
   applies to its DIRECT children. Every other tab's panel holds its cards
   directly, so they inherit it. This tab holds one wrapper div — so the
   five cards skills.js renders inside it were flex-gap's grandchildren and
   got no spacing at all. They had been touching since the tab was built.

   The wrapper has to forward the spacing itself. */
#skills-list {
  display: flex;
  flex-direction: column;
  gap: 20px;
}

/* ---- the page switcher ----
   Four count tiles, one per page. Deliberately NOT another tab strip: the
   profile tabs sit right above this and a second row of underlined tabs
   would compete with them. A tile carries a number, which a tab can't, and
   that number is the point — you see the shape of the whole tab before
   choosing where to go.

   The switcher card has no <h2>. It isn't a section of content; it's the
   control for the sections below it, so a heading would announce it as one
   more thing to read. */
.skill-switch { padding-top: 16px; padding-bottom: 16px; }
/* The shared rail shows three items across, sized as thirds — right for
   photos, wrong here.

   `minmax(190px, 1fr)` does two jobs at once, which a single value can't:

     1fr    — with four tiles they SHARE the row and fill it, so there's no
              dead space at the right-hand end.
     190px  — a floor. Once there are enough threads that 190px each no
              longer fits, they stop shrinking and the row overflows —
              which is what gives the arrows something to scroll.

   A fixed width filled nothing; a plain 1fr would shrink forever and the
   arrows would never enable. */
.skill-rail { margin-top: 0; }
/* The extra padding is NOT decoration. `.media-rail` is `overflow-x: auto`,
   and a scroll container CLIPS its children's shadows at its own edge — so
   without room below, the tiles' shadows would be sliced flat along the
   bottom. 12px is the shadow's reach. */
.skill-rail .media-rail {
  grid-auto-columns: minmax(190px, 1fr);
  gap: 10px;
  padding: 3px 2px 12px;
}
.skill-page {
  width: 100%;
  /* The Top skills tile has an empty meter row, so without this it would
     be shorter than the three that draw a bar. */
  height: 100%;
  display: flex;
  flex-direction: column;
  gap: 10px;
  padding: 14px 15px 15px;
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  background: var(--surface);
  text-align: left;
  cursor: pointer;
  /* RESTING LIFT. Two shadows, not one: a tight dark one for the contact
     edge and a wide soft one for the ambient falloff. A single blur can
     only be crisp or soft, and buttons need both to look solid. */
  box-shadow: 0 1px 2px rgba(0, 0, 0, .05), 0 3px 10px rgba(0, 0, 0, .06);
  transition: background .15s ease, border-color .15s ease,
              box-shadow .18s ease, transform .18s ease;
}
/* The lift and the shadow grow TOGETHER — a shadow that deepens while the
   object stays put reads as a light moving, not a button rising. */
.skill-page:hover {
  transform: translateY(-2px);
  box-shadow: 0 2px 4px rgba(0, 0, 0, .06), 0 8px 20px rgba(0, 0, 0, .10);
}
/* Pressed: back down, shadow almost gone. Faster than the rise, because
   real things fall quicker than they lift. */
.skill-page:active {
  transform: translateY(0);
  box-shadow: 0 1px 2px rgba(0, 0, 0, .08);
  transition-duration: .06s;
}
/* Name and value on one line, pushed to opposite ends.

   `align-items: baseline`, not center: the % is smaller than the name, and
   centring two different type sizes leaves the smaller one floating. Baseline
   sits them on the same line of type, which is what "aligned with the name"
   actually means. */
.skill-page-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 8px;
}
.skill-page-level {
  flex: none;
  font-size: 12px;
  color: var(--ink-soft);
  font-variant-numeric: tabular-nums;
}
.skill-page.is-active .skill-page-level { color: var(--accent); }
.skill-page-name {
  min-width: 0;   /* a flex child defaults to auto and refuses to ellipsis */
  font-size: 14px;
  /* 400 on ALL FOUR tiles now. Bold was doing two jobs — marking a heading
     and separating the threads from the entrance — and the second job is
     already done by the bar, the centring and the alignment. */
  font-weight: 400;
  color: var(--ink);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
/* Bar and number on ONE line, so the number reads as the bar's value
   rather than as a second fact stacked under the name. */
/* ---- the Top skills tile ----
   It measures nothing, so it gets no meter row and centres instead.
   Nothing else distinguishes it now — the weight came off every tile, so
   this rule is purely about position. */
.skill-page--top { align-items: center; justify-content: center; }

.skill-page-meter { display: flex; align-items: center; gap: 7px; }
.skill-page-bar {
  flex: 1;
  min-width: 0;
  height: 5px;
  border-radius: 999px;
  background: var(--line);
  overflow: hidden;
}
.skill-page-fill {
  display: block;
  height: 100%;
  border-radius: 999px;
  background: var(--accent);
  opacity: .45;                     /* recessive until this tile is the one you're on */
  transition: opacity .15s ease;
}
/* Nothing in the thread is rated yet. A word, not an empty bar — an empty
   bar would be claiming 0% expertise, which is a different statement from
   "you haven't said". */
.skill-page-unrated { flex: 1; min-width: 0; font-size: 11px; color: var(--ink-soft); }

/* ---- expertise, on a skill card ----
   Same anatomy as the tile's meter, one step larger, sitting above the
   sources divider: the level is part of the claim, the sources below are
   what back it. An unrated skill renders none of this. */
/* FC27: THE BAR TAKES THE WHOLE WIDTH, ON ITS OWN LINE. The number moved
   up beside the name, so the bar no longer shares a row with anything and
   `display: block` replaces the `flex: 1` it carried as a flex child.

   The wrapper row that used to hold the pair is gone with it — there is
   nothing left to lay out side by side.

   THE SAME BAR THE TOP SKILLS CARD DRAWS, on request, and each of the four
   numbers below was argued out over there rather than picked to match:

     7px, not 5      — at 5px on a pale track it reads as a hairline, not
                       as a measurement
     radius 2, not   — a 999px radius on a 7px bar is a capsule whose ends
     a full pill       curve away before the fill reaches them, so short
                       values read shorter than they are
     track --line,   — the empty half has to be VISIBLE or the bar states
     not --bg          no proportion, it is just a dash
     fill --meter,   — fifteen accent-blue bars made the measurement the
     not --accent      loudest thing on a card whose subject is the names

   Kept as two rules rather than shared with .skill-toprow-bar: the two
   bars agree today because the same reasoning applies to both, not
   because one is defined in terms of the other. */
.skill-level-bar {
  display: block;
  /* THE GAP UNDER THE HEAD ROW. 8 -> 10 with the card's padding, then
     10 -> 14 on its own.

     At 14 it is fractionally WIDER than the 16px of padding above the
     name and below the bar, which is deliberate: those two gaps are the
     card's edge, this one separates the claim from its measurement. A
     gap that merely equalled the padding would have read as a third
     equal band and left the card looking evenly striped rather than
     grouped. */
  margin-top: 14px;
  /* 7 -> 5. THE FREE HALF OF THE FIX: 29% less ink on the card at no cost
     whatever to the fill's contrast with its track — where transparency
     costs contrast for every step it buys. Height did most of the work
     that "solid, not minimal enough" was actually asking for.

     Still above the 4px where a bar starts reading as a hairline rather
     than as a measured quantity. */
  height: 5px;
  border-radius: 1px;
  background: var(--line);
  overflow: hidden;
}
/* The fill matches the track's radius, or a rounded fill sits inside a
   square end and the two disagree at the right edge. */
.skill-level-fill { display: block; height: 100%; border-radius: 1px; background: var(--meter-soft); }

/* ---- THE SCORE RAMP, APPLIED ----
   One rule for both bars: the skill cards and the Top skills rows draw
   the same measurement and were deliberately made identical, so they
   colour from the same tokens. --meter-soft survives as the fallback for
   anything that renders a bar without a band class. */
.skill-level-fill.is-band-1, .skill-toprow-fill.is-band-1 { background: var(--band-1); }
.skill-level-fill.is-band-2, .skill-toprow-fill.is-band-2 { background: var(--band-2); }
.skill-level-fill.is-band-3, .skill-toprow-fill.is-band-3 { background: var(--band-3); }
.skill-level-fill.is-band-4, .skill-toprow-fill.is-band-4 { background: var(--band-4); }
.skill-level-fill.is-band-5, .skill-toprow-fill.is-band-5 { background: var(--band-5); }
/* THE HEAD ROW: name left, number right. A wrapper around .skill-name
   rather than a change to it — .skill-name is how the rest of the site
   identifies a card, and putting a NUMBER inside it made its text read
   "Front-end architecture90%". The flag and logo the other two cards
   carry in there are images and contribute no text; a percentage does.

   `baseline`, not `center`: the name is 14px and the number 11px, and
   centring two sizes leaves the digits sitting fractionally high. */
.skill-head { display: flex; align-items: baseline; gap: 8px; }
/* Takes the slack, so the number is pushed to the right edge however long
   the name is. min-width: 0 lets it shrink below its content, which is
   what allows the ellipsis its own rule sets to actually fire. */
.skill-head .skill-name { flex: 1; min-width: 0; }
/* Pushed to the right edge by `margin-left: auto` — the same mechanism the
   language stars and the tool card's rung segments use, so all three land
   on one vertical line.

   Tabular figures so 65% and 100% take the same width and the column of
   numbers reads straight down the lane instead of shuffling by a digit.

   BLACK AND BOLD, on request. It went 400+soft, then 500+soft, and now
   600 in the same true black the name wears — the grey was reading as
   faint next to it, and this is the card's one number.

   SIZE IS NOW THE ONLY THING SEPARATING THE TWO: 11px against the name's
   14px. That is deliberate rather than left over. I had been using
   colour to keep the figure from competing with the name; with both in
   --ink-strong, the size gap is what remains, and it is enough — a small
   bold number beside a larger plain one reads as a value attached to a
   label, not as a second heading. Shrink that gap and the card gets two
   things fighting to be read first. */
.skill-level-num {
  margin-left: auto;
  flex: none;
  font-size: 11px;
  font-weight: 600;
  color: var(--ink-strong);
  font-variant-numeric: tabular-nums;
}
/* Accent border AND a tinted fill — border alone is too quiet to find at a
   glance, fill alone reads as a hover state. */
/* The selected tile sits highest, and its shadow is TINTED with the accent
   rather than grey — a blue-tinted shadow reads as "this one is lit",
   where a grey one would just look like it happened to be hovered. */
.skill-page.is-active {
  border-color: var(--accent);
  background: var(--accent-soft);
  box-shadow: 0 1px 2px rgba(0, 113, 227, .12), 0 6px 16px rgba(0, 113, 227, .18);
}
.skill-page.is-active:hover { transform: translateY(-2px); }

/* Keyboard focus. Every other interactive element on this site defines
   one — .level-badge, .sport-row, .sport-tourney-select — and these
   buttons were the exception, falling back to whatever the browser drew
   on top of the accent border. */
.skill-page:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

/* DARK MODE. Shadows are close to invisible against a dark surface, so the
   lift has to come from a light edge instead — the same trick the frosted
   sidebar uses. Without this the tiles would be flat rectangles in dark. */
[data-theme="dark"] .skill-page {
  box-shadow: 0 1px 2px rgba(0, 0, 0, .35), inset 0 1px 0 rgba(255, 255, 255, .05);
}
[data-theme="dark"] .skill-page:hover {
  box-shadow: 0 4px 14px rgba(0, 0, 0, .45), inset 0 1px 0 rgba(255, 255, 255, .08);
}
[data-theme="dark"] .skill-page.is-active {
  box-shadow: 0 4px 14px rgba(10, 132, 255, .30), inset 0 1px 0 rgba(255, 255, 255, .08);
}

/* Someone who has asked their system to reduce motion gets the shadow
   change without the movement. */
@media (prefers-reduced-motion: reduce) {
  .skill-page,
  .skill-page:hover,
  .skill-page:active,
  .skill-page.is-active:hover { transform: none; transition-duration: .01ms; }
}
.skill-page.is-active .skill-page-name { color: var(--accent); }
.skill-page.is-active .skill-page-fill { opacity: 1; }
.skill-page.is-active .skill-page-bar { background: rgba(0, 113, 227, .18); }

/* WAS a CSS grid, and the grid was the gap problem.

   A grid row is as tall as its tallest cell, so a card citing four sources
   set the row height and the two beside it ended early — leaving a band of
   dead space above the next row. `align-items: start` stopped them
   stretching but couldn't stop the row being tall.

   Now CardColumns deals them into .card-col columns, so a tall card only
   pushes down what is below it in its OWN column and every gap is the same
   20px. Same mechanism as Awards, Courses, Events, Projects and Licenses —
   and the same 20px, so the Skills tab stops being the one list with a
   tighter gap than everything else.

   `display: flex` is what this container needs above all: without it the
   .card-col divs are plain blocks and stack full-width, which is exactly
   the bug that gave Hobbies one card per row. */
.skill-cols {
  display: flex;
  align-items: flex-start;
  gap: 20px;
}
/* Languages' own stack — deliberately not CardColumns. A fixed single
   column, not one that only LOOKS vertical because its lane is currently
   too narrow for a second 260px column. See languagesHTML(). */
/* THE GAP IS SIZED BY THE HANDLE, not by taste. The disclosure chevron
   straddles each card's bottom border, so half of it — 9.5px of a 19px
   circle — hangs into this gap. The original 12px left barely a hairline
   before it touched the card below; 15px clears it by 5.5px while keeping
   the stack tight. Shrink this and the handle collides with the next card;
   grow the handle and this has to grow with it. */
.skill-lang-stack { display: flex; flex-direction: column; gap: 15px; }
/* Deliberately NOT .card: this sits INSIDE a card, so it takes a hairline
   and a flat surface rather than a second shadow on top of the first. */
.skill-card {
  padding: 12px 14px;
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  background: var(--surface);
}
/* MORE AIR IN THE WIDE LANE'S CARDS, on request — they read as cramped
   since the FC27 change, which is a real consequence of it rather than
   bad luck: the number moved out of a row of its own and up beside the
   name, so the card lost a line of height while keeping the same padding.
   12px was tuned around three stacked rows and is tight around two.

   16px puts equal air above the name and below the bar, so the pair sits
   centred in the card instead of drifting up.

   Scoped to .skill-cols, NOT set on .skill-card: the languages and tools
   cards in the narrow lane are the same class, and they still carry three
   rows of content in the space this is loosening. Only the wide lane's
   cards lost a row, so only they get the padding back.

   The HORIZONTAL padding deliberately stays 14px — the disclosure handle
   is anchored at `right: 14px` to line its edge up with the card's own
   padding, and changing one without the other silently breaks that. */
.skill-cols .skill-card { padding: 16px 14px; }
/* NORMAL WEIGHT, TRUE-BLACK INK. Two separate settings, arrived at
   separately, and the order they were tried in is the useful part:

     600 + --ink      too bold, and read as grey
     400 + --ink      weight fixed, still read as grey
     500 + --ink      grey again — the weight was never the cause
     400 + --ink-strong   ← here. Plain weight, actually black.

   The lesson worth keeping: the greyness was the COLOUR the whole time.
   --ink is #1d1d1f, roughly 88% of the way to black, and three rounds of
   adjusting weight could not fix a problem that was never about weight.
   The name carries no emphasis of its own — it sits above a rating and a
   pile of evidence that supply all the emphasis the card needs.

   THE COLOUR IS --ink-strong, NOT --ink. --ink is #1d1d1f, a deliberate
   near-black, and it was what these names inherited all along — but at
   500 on a white card it still read as grey. #1d1d1f is roughly 88% of
   the way to black; on a short label with no surrounding text to compare
   it against, that last 12% is visible. Body copy keeps --ink, where the
   softer black is easier over many lines.

   Still a TOKEN and never a literal #000: --ink-strong inverts to #fff in
   dark mode, where a hardcoded black would turn these names invisible.

   The languages stack reached this exact conclusion first and carried its
   own override for it. The thread pages have now made the same call, so
   the override folded back in here and every skill name on the tab —
   language, tool, skill — is one rule again. */
.skill-name {
  display: flex;
  align-items: center;
  gap: 6px;
  font-size: 14px;
  font-weight: 400;
  color: var(--ink-strong);
}
.skill-note { margin-top: 4px; font-size: 12px; color: var(--ink-soft); line-height: 1.5; }
.skill-srcs {
  list-style: none;
  display: flex;
  flex-direction: column;
  gap: 8px;
  margin-top: 10px;
  padding-top: 10px;
  border-top: 1px solid var(--line);
}
.skill-src { display: flex; align-items: center; gap: 8px; min-width: 0; }
/* The logo sits over the icon, so an entry without one still shows the
   kind of source it is instead of an empty square. */
.skill-src-logo {
  position: relative;
  flex: none;
  width: 26px;
  height: 26px;
  border-radius: 6px;
  border: 1px solid var(--line);
  background: var(--bg);
  display: flex;
  align-items: center;
  justify-content: center;
  overflow: hidden;
  color: var(--ink-soft);
}
.skill-src-logo svg { width: 13px; height: 13px; }
.skill-src-logo img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: contain;
  background: var(--surface);
}
/* min-width:0 on BOTH the flex child and its children, or the ellipsis
   never fires — flex items default to min-width:auto. */
.skill-src-text { display: flex; flex-direction: column; min-width: 0; }
.skill-src-title,
.skill-src-sub { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.skill-src-title { font-size: 13px; }
.skill-src-sub { margin-top: 1px; font-size: 11px; color: var(--ink-soft); }
/* A link whose entry was deleted elsewhere. Visible on purpose — a
   dangling link that hid itself would be a fact quietly disappearing. */
.skill-src.is-broken .skill-src-logo { border-style: dashed; }
.skill-src.is-broken .skill-src-title { color: var(--ink-soft); font-style: italic; }
/* The overflow disclosure. The hidden rows are a second .skill-srcs, so
   they need no top border of their own — the first list already drew the
   rule that separates sources from the skill name. */
.skill-more { margin-top: 8px; }
.skill-more > summary {
  cursor: pointer;
  font-size: 11px;
  color: var(--accent);
  list-style: none;
}
.skill-more > summary::-webkit-details-marker { display: none; }
.skill-more > summary::after { content: " ▾"; }
.skill-more[open] > summary::after { content: " ▴"; }
.skill-more .skill-srcs { margin-top: 8px; padding-top: 0; border-top: none; }
.skill-nosrc {
  margin-top: 10px;
  padding-top: 10px;
  border-top: 1px solid var(--line);
  font-size: 11px;
  color: var(--ink-soft);
}
/* The Top skills strip sits above the main card and needs to read as a
   summary of it, not as a sixth group. */
.skill-top .skills-note { margin-bottom: 14px; }
/* Second and later sections need air above their heading; the first
   doesn't, or it would double up with the card's own top padding. */
.skill-top .skill-cat { margin-top: 18px; }
.skill-top .skill-cat:first-of-type { margin-top: 0; }

/* THE SAME 2fr/1fr SPLIT #sports-list ALREADY USES for its profile-card
   and career-card pair — matched exactly, on request, rather than
   inventing a new proportion for what is structurally the same idea: one
   wide primary card and one narrow column of smaller ones beside it.

   `align-items: start` is what stops the short column being stretched to
   match the tall one — the same rule that made #sports-list need it, for
   the same reason: a grid row defaults to matching its tallest cell. */
.skill-top-pair {
  display: grid;
  /* Widened from Sports' own 2fr/1fr (66/33) to 3fr/2fr (60/40) on
     request, then trimmed to 8fr/5fr (61.5/38.5) once the radar's viewBox
     was cropped to its real content.

     THOSE TWO NUMBERS ARE A PAIR — changing one without the other resizes
     the chart. The SVG is width:100%, so it always fills this column: what
     the chart renders at is column-width ÷ viewBox-width. The viewBox lost
     the same ~4.5% of dead margin that this column lost of width, so the
     ratio is unchanged and the chart draws at exactly the size it did
     before — the card just stops carrying empty space around it. Narrow
     this column further on its own and the chart shrinks with it. */
  grid-template-columns: minmax(0, 8fr) minmax(0, 5fr);
  align-items: start;
  gap: 20px;
}
/* Skill shape above Tools in the narrow column — the same job .sport-stack
   does for the club and formation cards on the Sports tab. */
.skill-top-stack { display: flex; flex-direction: column; gap: 20px; min-width: 0; align-self: start; }
/* Same breakpoint #sports-list uses: below this, "wide column, narrow
   column" stops making sense on a phone-width screen. */
@media (max-width: 860px) { .skill-top-pair { grid-template-columns: minmax(0, 1fr); } }

/* ---- the skill profile radar ----
   Six named skills, two per thread, plotted on self-rated expertise — the
   only bounded, comparable number this tab has that needs no population
   to rank against, unlike a real player radar. Evidence count was tried
   and rejected as the axis value: one skill on this exact chart carries
   45 sources against 3–5 for its neighbours, which would flatten five
   axes to plot the sixth honestly.

   Every colour comes off the site's own tokens rather than a fixed hex,
   so the chart repaints correctly in dark mode with zero extra rules. */
.skill-radar { padding-bottom: 18px; }
/* Title and note stay LEFT-aligned like every other card on this tab —
   only the chart itself centres, via its own margin rather than a
   flex/align-items rule on the whole card that used to pull the text
   along with it. */
/* THIS 360 MUST MATCH THE VIEWBOX WIDTH IN skills.js. Everything inside an
   SVG — font-size included — is scaled by rendered-width ÷ viewBox-width,
   because the browser applies one transform to the whole subtree. Holding
   the two at the same number pins that ratio at exactly 1, so a font-size
   of 12px below really is 12px on screen. (They used to disagree — 200
   units drawn 360px wide — and that hidden 1.8× is what made every attempt
   to size these labels overshoot or undershoot. Narrower screens scale the
   whole chart down together, which is fine; it's the mismatch that bites.) */
.radar-svg { display: block; width: 100%; max-width: 344px; margin: 2px auto 0; }
/* Rules off the header from the chart, the same job every other card's
   divider does elsewhere on the site — border on the element below,
   never a stray line with nothing under it. */
.skill-radar-divider { margin: 14px 0 4px; border: none; border-top: 1px solid var(--line); }
/* The graticule the shape is read against: a spoke per axis and rings at
   100% and 50%. Solid, not dashed — with a second ring inside it the
   gradation is self-evident, and dashes at this weight just read as noise.
   The inner ring is held back so the outer one still reads as the target. */
.radar-spoke, .radar-ring { stroke: var(--line); stroke-width: 1; fill: none; }
.radar-ring-inner { opacity: .55; }
.radar-fill { fill: var(--accent); fill-opacity: .22; stroke: var(--accent); stroke-width: 1.5; }
/* FotMob's own weighting, and it's the opposite of what this had: the
   NUMBER carries the emphasis (bold, full-strength ink) and the skill name
   is quiet supporting text. Bold names competed with the shape for
   attention and made the whole card feel busy; this way the eye lands on
   the six figures, then reads what they belong to. */
.radar-label { font-size: 12px; fill: var(--ink-soft); font-weight: 400; }
.radar-label-value { font-size: 13px; fill: var(--ink); font-weight: 700; font-variant-numeric: tabular-nums; }
/* An unrated axis must not shout. "Not rated" wearing the number's bold
   emphasis would give the loudest treatment on the card to the one axis
   with nothing to say, so it drops back to the name's quiet weight. */
.radar-label-value.is-unrated { font-size: 12px; fill: var(--ink-soft); font-weight: 400; }

/* ---- the Top skills rows ----
   The FotMob layout he pointed to, adapted to what one profile can
   honestly measure: label, its thread, and — only once rated — a bounded
   0–100% bar. FotMob's own bar is RANK against thousands of other
   players; there is no population of other Izzats to rank against, and
   raw evidence count was tried and rejected — one skill (45 sources)
   would flatten every other bar in its section to a sliver. So the bar
   here is self-rated expertise, the same number the switcher tiles use,
   and it simply doesn't appear until a rating exists. */
.skill-toplist { list-style: none; display: flex; flex-direction: column; }
/* NO ROW DIVIDERS. FotMob separates its rows with space alone, and a rule
   between every line is what made this card read as busy — ten skills
   became ten boxes. Spacing does the same job silently. */
.skill-toprow {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 8px 0;
}
/* The name takes the slack, which is what pushes the thread column over to
   sit against the bar however short or long the name is. */
.skill-toprow-name { flex: 1; min-width: 0; font-size: 14px; color: var(--ink); }
/* The thread now occupies the column the percentage used to, right-aligned
   against the rule so every divider lands on one vertical line. Width is
   set by the longest of the three — "Entrepreneurship" — since a column
   that ellipsised its own contents would be worse than no column. */
.skill-toprow-thread {
  flex: none;
  width: 112px;
  padding-right: 10px;
  margin-right: 10px;
  border-right: 1px solid var(--line);
  text-align: right;
  font-size: 12px;
  color: var(--ink-soft);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
/* Present only for a RATED skill — see topSkillRowHTML(). An unrated row
   simply ends after the thread name; there is no empty slot reserved for
   a bar that isn't there, unlike the switcher tiles, which need every
   tile the same height. A list of rows has no such constraint. */
/* Wider than the old 130px: this card moved into the WIDE lane, and a
   short bar in a long row made the number look stranded out on the right.
   Capped on narrow screens, where the pair sits in one column. */
.skill-toprow-meter { position: relative; flex: none; display: flex; align-items: center; width: 150px; }
@media (max-width: 700px) { .skill-toprow-meter { width: 130px; } }
/* THE PERCENTAGE, ON HOVER. Drawn on the meter rather than the bar,
   because the bar's overflow:hidden — which clips its own fill — would
   clip this too. Instant, unlike a title attribute's second-long delay,
   and styled to match the site instead of the OS. */
.skill-toprow-meter::after {
  content: attr(data-rating);
  position: absolute;
  bottom: calc(100% + 5px);
  right: 0;
  padding: 2px 7px;
  border-radius: 5px;
  background: var(--ink);
  color: var(--surface);
  font-size: 11px;
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
  opacity: 0;
  /* Never intercepts the pointer, or the tooltip appearing under the
     cursor would itself end the hover and make it flicker. */
  pointer-events: none;
  transition: opacity .12s ease;
}
.skill-toprow-meter:hover::after { opacity: 1; }
@media (prefers-reduced-motion: reduce) { .skill-toprow-meter::after { transition: none; } }
/* Thicker and fully rounded, like FotMob's. At 4px on a pale track it read
   as a hairline rather than a measurement — which is what "ugly bar" was
   pointing at. The track has to be visible for the fill to mean anything:
   a bar you can't see the empty half of isn't a proportion, it's a dash. */
/* 999 -> 2 -> 1, each step for the same reason: ROUNDING EATS THE ENDS.
   A radius curves the fill away before it reaches the tip, so a short
   value reads shorter than it is — at 999px a 7px bar was a capsule and
   the first and last few points of every rating were geometry rather
   than data.

   1px on a 5px bar is a softened corner, not a shape. It is the last
   step before square; 0 is available and would measure identically,
   since a 1px radius rounds away roughly a quarter of one pixel. */
.skill-toprow-bar {
  flex: 1;
  /* 7 -> 5, with the skill card's. The two bars were deliberately made
     identical and there is a test comparing them property by property —
     softening one and not the other would put the tab back to drawing
     the same measurement two different ways. */
  height: 5px;
  border-radius: 1px;
  background: var(--line);
  overflow: hidden;
}
/* Green, but the muted --meter sage rather than a signal green — see the
   token. Not the accent blue: ten accent bars made the measurement the
   loudest thing on a card whose subject is the skill names, the same
   reason the section headings lost the accent. */
.skill-toprow-fill { display: block; height: 100%; border-radius: 1px; background: var(--meter-soft); }
/* Still used by the TOOLS card, which prints its number after the bar
   rather than hiding it behind a hover — the Top skills row no longer
   renders this at all. */
.skill-toprow-level {
  flex: none;
  margin-left: 8px;
  font-size: 11px;
  color: var(--ink-soft);
  font-variant-numeric: tabular-nums;
}

/* ---- Tools card: a HORIZONTAL RAIL ----
   Reuses the switcher's rail wholesale; only the tile inside it is new.
   The extra bottom padding is NOT decoration — .media-rail is
   overflow-x:auto, and a scroll container clips its children's shadows at
   its own edge, so without room the tiles would be sliced flat. */
/* The arrows are pulled OUT past the card's own 24px padding, so they sit
   near its edges instead of crowding the tiles. -14px of the 24 keeps
   10px of breathing room at the card's border; the wrap's own gap then
   closes back up so the rail reclaims the width the arrows gave up.

   The negative margin is why this is scoped to .tool-rail and not applied
   to .media-nav globally — every other rail on the site sits inside a
   different container, and pulling their arrows outward would hang them
   over whatever happens to be beside them. */
.tool-rail { margin-top: 4px; gap: 4px; }
.tool-rail > .media-nav:first-child { margin-left: -14px; }
.tool-rail > .media-nav:last-child { margin-right: -14px; }
/* Smaller than the 30px default: these sit beside 140px tiles, and at
   full size they read as the two heaviest objects on the card. */
.tool-rail .media-nav { width: 24px; height: 24px; }
.tool-rail .media-nav svg { width: 13px; height: 13px; }
.tool-rail .media-rail {
  /* NO grid-auto-columns override, deliberately. It used to set
     `minmax(158px, 1fr)` — a MINIMUM width, which does not divide the rail
     evenly: three tiles took their 158px and the remainder piled up as
     dead space at the right end, so the gap before the next arrow was
     visibly wider than the one after the previous arrow.

     The base .media-rail already computes `calc((100% - 2 * 12px) / 3)`,
     which makes exactly three columns consume the full width with nothing
     left over. Inheriting it costs a fixed tile width — the tiles now flex
     with the rail — and buys symmetry, which is the thing that was wrong.

     Only the padding is overridden: .media-rail is overflow-x:auto, and a
     scroll container clips its children's shadows at its own edge, so the
     12px below is the shadow's reach. */
  padding: 3px 2px 12px;
}
/* Not a button, unlike the switcher's tiles — a tool has nowhere to
   navigate to, and a control that does nothing on click is worse than
   plain text. `height: 100%` keeps every tile level whether its meter
   holds a bar or the words "Not rated". */
.tool-tile {
  height: 100%;
  display: flex;
  flex-direction: column;
  /* This gap is what sets the tile's height. The segment row also carries
     `margin-top: auto`, but that only absorbs SPARE space — and now every
     tile holds the same two rows, there is none to absorb. So the gap is
     the whole distance between the logo block and the bar, and raising it
     raises the card. */
  gap: 16px;
  padding: 12px;
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  background: var(--surface);
}
/* Logo left, text right. `align-items: center` rather than flex-start so
   a one-line name still sits level with the logo's middle. */
.tool-tile-head { display: flex; align-items: center; gap: 10px; min-width: 0; }
/* min-width:0 is load-bearing on BOTH this and the name: a flex item
   defaults to min-width:auto, which refuses to shrink below its content,
   so without it a long name pushes the tile wider instead of ellipsising. */
.tool-tile-text { min-width: 0; display: flex; flex-direction: column; gap: 1px; }
.tool-tile-name {
  font-size: 14px;
  color: var(--ink);
  /* A long tool name truncates rather than wrapping the tile taller than
     its neighbours — the full name is in the tile's aria-label. */
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
/* Pushed to the bottom, so the bars line up across the rail no matter how
   the names above them differ. */
/* FIVE SEPARATE PIECES, not one bar filled part-way. The gaps are the
   honest bit: a continuous bar at 3/5 implies the value could have landed
   anywhere in between, and this scale has five rungs and nothing between
   them. Each segment flexes, so the row fills the tile at any width. */
.tool-tile-seg { margin-top: auto; display: flex; gap: 3px; min-width: 0; }
.tool-seg {
  flex: 1;
  height: 7px;
  border-radius: 1px;
  background: var(--line);
}
/* THE SAME SCORE RAMP, ON THE RUNGS. Five Dreyfus rungs against five
   score bands is a 1:1 map, so a tool's colour means what a skill's
   colour means and the tab has one scale rather than two.

   Measured separately from the bar, because the blend differs: the bar's
   fill is a child sitting ON the track, so it blends with --line; a
   segment IS the element and replaces its own background, so it blends
   with the CARD. Same alpha, different backdrop. Filled-vs-unfilled at
   90% comes out 4.41 / 3.35 / 3.14 / 3.11 / 3.34 in light and 3.12 /
   3.85 / 5.10 / 5.12 / 4.57 in dark — all clear.

   --meter survives as the fallback for an on-segment with no band. */
.tool-seg.is-on { background: var(--meter); }
.tool-seg.is-on.is-band-1 { background: var(--band-1); }
.tool-seg.is-on.is-band-2 { background: var(--band-2); }
.tool-seg.is-on.is-band-3 { background: var(--band-3); }
.tool-seg.is-on.is-band-4 { background: var(--band-4); }
.tool-seg.is-on.is-band-5 { background: var(--band-5); }

/* ---- THE BAND LEGEND, in Settings only ----
   Five colours appear on the cards with nothing on the page to explain
   them. A legend beside every bar would be noise on a card whose subject
   is the skill name; Settings is the one place you are actually choosing
   a number, so it belongs there and only there.

   The swatch is a bar, not a dot — it is a sample of the thing it
   explains, at the height that thing is actually drawn. */
.sk-band-key {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  margin-right: 10px;
  white-space: nowrap;
}
.sk-band-dot {
  display: inline-block;
  width: 16px;
  height: 5px;
  border-radius: 1px;
  /* Same fallback the fills carry, for a legend row with no band class. */
  background: var(--meter-soft);
}
.sk-band-dot.is-band-1 { background: var(--band-1); }
.sk-band-dot.is-band-2 { background: var(--band-2); }
.sk-band-dot.is-band-3 { background: var(--band-3); }
.sk-band-dot.is-band-4 { background: var(--band-4); }
.sk-band-dot.is-band-5 { background: var(--band-5); }
/* The rung's NAME, on the line the thread used to occupy. Five filled
   segments out of five mean nothing on their own, and the legend that
   explains the scale lives in Settings, not on the card. */
.tool-tile-tier {
  font-size: 11px;
  color: var(--ink-soft);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
/* Italic marks "Not rated" as a STATE rather than one of the rungs — the
   five real names are all set upright. */
.tool-tile-tier.is-unset { font-style: italic; }
/* The slot keeps its width even when empty, so names line up whether or
   not a logo has been uploaded yet — otherwise the list's left edge would
   go ragged as logos get added one at a time. `flex: none` stops a long
   tool name squashing it. `object-fit: contain` keeps a non-square logo in
   proportion rather than stretching it to fill. */
.tool-logo {
  position: relative;
  flex: none;
  width: 38px;
  height: 38px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border-radius: 8px;
  background: var(--bg);
  border: 1px solid var(--line);
  color: var(--ink-soft);
  overflow: hidden;
}
/* The placeholder icon, sitting under any uploaded image. */
.tool-logo svg { width: 18px; height: 18px; }
/* contain, not cover — a wordmark or a non-square logo is letterboxed
   rather than cropped, and it covers the icon underneath when it loads. */
.tool-logo img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: contain; }

/* ---- languages ----
   Each language is a name plus a level, so it gets a row of its own with
   the name leading. A dl/dt/dd grid made the language look like a field
   LABEL, which read as though "Malay" were a category rather than a
   thing you speak. */
.lang-list { list-style: none; display: flex; flex-direction: column; }
.lang-item {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 20px;
  padding: 13px 0;
  border-top: 1px solid var(--line);
}
.lang-item:first-child { border-top: none; padding-top: 2px; }
.lang-name { font-size: 16px; color: var(--ink); }
/* The proficiency, now inside a .skill-card rather than at the right-hand
   end of a row — so it loses the right alignment and takes the same size
   and colour as a skill's note. It sits ABOVE the divider, because it
   describes the language; the certificate below it is the evidence.

   The three rules above are kept: .lang-list / .lang-item are still the
   right shape for a plain two-column list, and nothing is gained by
   deleting them while the print stylesheet and future layouts may want
   them back. */
/* The flag is a flex child of .skill-name, so that row's existing 6px gap
   spaces it off the name with no extra rule. `flex: none` is the load-
   bearing bit: without it a long language name would squash the flag
   instead of wrapping, since flex items shrink before they wrap.
   The radius lives here rather than on the <svg> so that `overflow:
   hidden` can actually clip the artwork's square corners. No ring around
   it — the flags carry their own edges, and a border on top of a white
   stripe read as part of the flag rather than as a frame. */
.lang-flag {
  flex: none;
  display: inline-flex;
  width: 20px;
  height: 10px;
  border-radius: 2px;
  overflow: hidden;
}
.lang-flag svg { width: 100%; height: 100%; display: block; }
/* A language name is a plain label, not a heading — it sits beside a flag
   and a rating that already carry the emphasis, so it takes neither. That
   used to need an override here, because .skill-name was semibold for the
   thread pages. It isn't any more: the base rule makes the same call for
   every card on the tab, so this stack no longer states it separately. */
/* The handle straddles the card's bottom border, the same shape Licenses
   and Awards use. Being absolutely positioned it takes NO space in the
   card's flow — which is the whole reason it replaced the in-card button:
   a control on a card this small shouldn't spend interior width.

   22px, not the 26px Licenses uses. These cards are roughly a third the
   height of a licence card, and half the handle hangs BELOW the card into
   the stack's gap — see .skill-lang-stack's gap note. */
/* Scoped to .skill-card, not .skill-lang-stack, since the ordinary skill
   cards in the WIDE lane grew the same handle. One rule now dresses all
   three — languages, tools, and skills — which is the point: they are the
   same control doing the same job on three cards that already share a
   shape. `position: relative` moved onto .skill-card with it, so the
   anchor travels with the cards rather than with one of their containers.
   A separate lang-card hook used to hold that anchor; every card here is
   already a skill card, so it was one class doing nothing extra and both
   the rule and the class it named are gone. */
.skill-card { position: relative; }
.skill-card .timeline-toggle.lang-toggle {
  position: absolute;
  /* Right-anchored, not centred like Licenses' — and 14px is the card's own
     horizontal padding, so the handle's right edge lands on the same
     vertical line the stars end on. It reads as sitting under the rating
     rather than floating at an arbitrary inset. */
  right: 14px;
  bottom: 0;
  transform: translateY(50%);
  width: 19px;
  height: 19px;
  display: flex;
  align-items: center;
  justify-content: center;
  margin: 0;
  border-radius: 50%;
  border: 1px solid var(--line);
  background: var(--surface);
  box-shadow: var(--shadow);
  color: var(--ink-soft);
  /* Above the card it sits on, or the card's own background paints over
     the half that overlaps it and the circle reads as a semicircle. */
  z-index: 1;
}
.skill-card .timeline-toggle.lang-toggle:hover {
  color: var(--ink);
  border-color: var(--ink-soft);
}
.skill-card .timeline-toggle.lang-toggle svg {
  width: 11px;
  height: 11px;
  display: block;
  transition: transform .2s ease;
}
/* Rotating the same chevron 180° reads as "click to collapse" without
   swapping in a second icon — the same trick the Licenses handle uses. */
.skill-card .timeline-toggle.lang-toggle.is-open svg { transform: rotate(180deg); }
/* The list's own divider is inside the collapsed panel, so it appears with
   the certificates and vanishes with them — no stray line above nothing. */
.skill-card .timeline-more .skill-srcs { margin-top: 10px; }
/* A language with no flag in the table keeps the SAME footprint, empty —
   so every name on the card still starts at one shared left edge instead
   of jumping left when a flag happens to be missing. */
.lang-flag.is-unset { box-shadow: 0 0 0 1px var(--line); background: transparent; }

/* The tool stack on a THREAD page — the Languages card's shape carrying a
   tool. The segments take the place the language stars occupy, pushed
   right by the same auto margin so the two cards' right edges align. */
.tool-stack-seg { margin-left: auto; flex: none; display: flex; gap: 3px; width: 62px; }
/* "Not rated" is a STATE, not a sixth rung — italic marks it apart from
   the five real names, exactly as it does on the rail's tiles. */
.lang-level.is-unset { font-style: italic; }
/* The stack's logo sits inline with the name, so it needs the language
   flag's alignment rather than the rail tile's larger block. */
.skill-lang-stack .tool-logo { width: 22px; height: 22px; border-radius: 5px; }
.skill-lang-stack .tool-logo svg { width: 12px; height: 12px; }

/* PROFICIENCY AS FIVE STARS. `margin-left: auto` inside the flex row that
   .skill-name already is does all the layout work — it eats the leftover
   space, pinning the name left and the rating hard right however long the
   name happens to be. letter-spacing opens the glyphs up, which otherwise
   crowd into a solid block at this size. */
.lang-stars { margin-left: auto; display: inline-flex; letter-spacing: 1px; font-size: 15px; }
/* Unearned stars stay PRESENT but recede — showing the empty ones is what
   tells you the scale is out of five. A missing star and a grey one say
   different things; only one of them is honest about a rating of two. */
.lang-star { color: var(--line); }
.lang-star.is-on { color: #e8a33d; }
/* Amber is a literal, not a token, and deliberately so: every other token
   here is a THEME colour that flips between light and dark, but a rating
   star is gold in both — swapping it would read as a different state. It
   is lifted slightly in dark mode only to hold its brightness against the
   darker card, not to change hue. */
[data-theme="dark"] .lang-star.is-on { color: #f5b942; }
.lang-level { margin-top: 4px; font-size: 12px; color: var(--ink-soft); line-height: 1.5; }
/* .lang-note is gone with the level legend it spaced — the legend was
   removed from the Languages card on request, and a rule whose only
   element no longer exists is dead weight that reads as still-in-use. */

@media (max-width: 500px) {
  .lang-item { flex-direction: column; gap: 2px; }
  .lang-level { text-align: left; }
}

/* ---- credential list: certifications, awards, test scores ----
   One component, three uses. Same fields, same shape — so it gets one
   set of styles rather than three that drift apart. */
.cred-list { list-style: none; display: flex; flex-direction: column; }
.cred-item {
  display: flex;
  gap: 14px;
  padding: 18px 0;
  border-top: 1px solid var(--line);
}
.cred-item:first-child { border-top: none; padding-top: 4px; }
.cred-logo {
  position: relative;
  width: 42px; height: 42px;
  border-radius: var(--radius-sm);
  background: var(--bg);
  border: 1px solid var(--line);
  display: flex;
  align-items: center;
  justify-content: center;
  flex-shrink: 0;
  color: var(--ink-soft);
  overflow: hidden;
}
.cred-logo svg { width: 20px; height: 20px; }
/* Same reasoning as .timeline-logo: contain, not cover, so an uploaded
   logo never gets cropped, and the icon stays underneath as the fallback
   for entries with no logo uploaded yet. */
.cred-logo img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: contain; }
/* flex:1 so the body claims the rest of the row. Without it the body is
   only as wide as its content, and the auto-fit .cred-detail grid inside
   never gets the width it needs to form extra columns. */
.cred-body { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 3px; }
.cred-title { font-size: 16px; font-weight: 600; letter-spacing: -0.01em; }
.cred-sub { font-size: 14px; color: var(--ink); }
.cred-meta { font-size: 13px; color: var(--ink-soft); }
.cred-desc { font-size: 14px; color: var(--ink-soft); margin-top: 4px; }
.cred-detail {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
  gap: 8px 20px;
  margin-top: 8px;
}
.cred-detail dt { font-size: 12px; color: var(--ink-soft); }
.cred-detail dd { font-size: 13px; margin: 0; }
.cred-link {
  align-self: flex-start;
  margin-top: 8px;
  font-size: 13px;
  color: var(--accent);
  text-decoration: none;
}
.cred-link:hover { text-decoration: underline; }

/* A short note under a section heading explaining what belongs there —
   used where two sections are easy to confuse (Courses vs Licenses). */
.section-note {
  font-size: 13px;
  margin: -4px 0 16px;
}

/* A labelled sub-block inside a section (e.g. Languages within Identity) */
.info-subhead {
  font-size: 13px;
  font-weight: 500;
  color: var(--accent);
  margin: 22px 0 12px;
  padding-top: 18px;
  border-top: 1px solid var(--line);
}

/* ---- media attachments ----
   LinkedIn-style: any entry can carry links, images, PDFs or video.
   One component, used on every kind of entry — timelines, credentials,
   projects, publications. Copy the <ul> and it works anywhere, because
   nothing here depends on which section it sits in. */
.media-list {
  list-style: none;
  display: flex;
  flex-wrap: wrap;
  gap: 10px;
  margin-top: 14px;
}
.media-item { max-width: 260px; }
.media-item a {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 8px 12px 8px 8px;
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  background: var(--surface);
  text-decoration: none;
  color: inherit;
  transition: background .15s ease, border-color .15s ease;
}
.media-item a:hover { background: var(--bg); border-color: var(--ink-soft); }
.media-thumb {
  width: 38px; height: 38px;
  border-radius: 8px;
  background: var(--bg);
  display: flex;
  align-items: center;
  justify-content: center;
  flex-shrink: 0;
  color: var(--ink-soft);
}
.media-thumb svg { width: 18px; height: 18px; }
/* An uploaded image fills the same slot the icon used — the icon stays as
   the fallback for video items (never uploaded, always a link) and for an
   image row before anything's been picked yet. */
.media-thumb img { width: 100%; height: 100%; object-fit: cover; border-radius: 8px; }
.media-text { display: flex; flex-direction: column; gap: 1px; min-width: 0; }
.media-title {
  font-size: 13px;
  color: var(--ink);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.media-kind { font-size: 11px; color: var(--ink-soft); }

/* ---- career: one card per COMPANY ----
   The card header is the employer; the roles held there are nested inside as
   positions. Grouping by company is what makes a promotion read correctly —
   two positions under one employer instead of two disconnected cards. */
/* The six cards used to be the panel's own children, so .profile-panel's
   `gap: 20px` sat between them. They live inside #career-list now — one flex
   child holding all six — so that gap fell between the list and nothing, and
   the cards collapsed onto each other. The list has to carry the same column
   layout the panel was giving them. */
#career-list { display: flex; flex-direction: column; gap: 20px; }
.career-card { padding: 22px 24px; }
.career-card:hover { transform: none; box-shadow: var(--shadow); }

/* ---- education: one card per INSTITUTION ----
   Same shape as Career's #career-list, for the same reason: every school and
   university used to be one <li> inside one shared card (.timeline inside a
   single .profile-section.card), so the whole Education tab read as one
   long list rather than a set of separate chapters. Now each institution is
   its own .card.edu-card, and .edu-list carries the gap between them —
   .profile-panel's own `gap:20px` only applies between ITS direct children
   (the heading section and this list), not between cards nested inside it. */
.edu-list { display: flex; flex-direction: column; gap: 20px; }
.edu-card { padding: 22px 24px; }
.edu-card:hover { transform: none; box-shadow: var(--shadow); }

/* ---- events: one card per EVENT ----
   .cred-item was built as a bordered ROW meant to sit inside one shared
   .cred-list card (a divider between rows, no border/shadow of its own) —
   Skills still uses it exactly that way, and keeps doing so untouched.
   Events moved to its own separate .card per entry, the same reasoning as
   Career/Education above, so this override only fires once .card is ALSO
   on the element — it never touches bare .cred-item. Courses, Licenses,
   Projects and Awards all made the same move afterwards, each with their
   own grid (Projects joins this one — same content richness as Courses/
   Events, so the same 2-per-row math applies; Awards' own 3-per-row rule
   sits just below). */
/* Two per row on this page's own 1100px .profile-main — three would leave
   each card too narrow for its own detail grid/media list to breathe.
   auto-fit (not a hardcoded 2) so it still collapses to one column on a
   narrower viewport without a separate breakpoint rule. align-items:start
   so cards keep their own height instead of stretching to match a taller
   neighbour in the same row — normal for a card grid with variable content. */
#events-list, #courses-list, #projects-list {
  display: flex;
  align-items: flex-start;
  gap: 20px;
}
/* Awards: three per row, on request. 300px, not the 320px tried first —
   the column budget is exact and 320 misses it. .profile-main is
   max-width 1100px with 56px of padding each side, so the real content
   width is 988px; three tracks plus two 20px gaps needs 3×track + 40 ≤
   988, i.e. a track of at most 316px. minmax(320px, …) asked for 1000px
   and silently wrapped to two columns. 300px clears it with room to
   spare — the same number Licenses landed on for the same reason.
   Its own separate rule, not joining the shared one above: a genuinely
   different column count needs its own minmax, not a shared breakpoint.
   auto-fill, not auto-fit, so a row that isn't full still keeps its
   empty track instead of stretching the cards that are there to swallow
   it. align-items: start for the same reason Courses/Events/Projects use
   it — only the card someone actually opens should grow. */
/* Independent columns, not a grid. A grid row is as tall as its tallest
   cell, so expanding one card left a hole beside every other card in that
   row. Here a card can only push down what is below it in its OWN column.
   awards.js deals the cards out round-robin so the reading order across
   the top row still matches the sort. */
/* Every container CardColumns deals into needs this. Without it the
   .card-col divs are plain blocks and stack vertically at full width —
   which is exactly what happened to Hobbies when this rule listed one
   fewer id than there were lists. */
#awards-list,
#hobbies-list {
  display: flex;
  align-items: flex-start;
  gap: 20px;
}
/* One shared column class for every card list — Courses, Events,
   Projects, Licenses and Awards all deal their cards into these.

   flex: 1 1 0 (not 1 1 auto) so all columns share the width equally
   regardless of their content — otherwise the column holding the tallest
   or widest card would claim more than its share.
   min-width: 0 because a flex item defaults to min-width: auto and would
   otherwise refuse to shrink below its widest card. */
.card-col {
  flex: 1 1 0;
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: 20px;
}
/* ---- Awards: the group filter bar ----
   Sits between the tabs and the grid, so it reads as a second, narrower
   level of navigation under them. Deliberately lighter than .profile-tab:
   these are chips that sit ON the page background, not another tab strip
   competing with the real one above. wrap, because the group list grows
   with the record and there's no room to assume one line. */
.award-filters {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
}
.award-filter {
  display: inline-flex;
  align-items: center;
  gap: 7px;
  padding: 6px 12px;
  border: 1px solid var(--line);
  border-radius: 8px;
  background: var(--surface);
  color: var(--ink-soft);
  font: inherit;
  font-size: 13px;
  cursor: pointer;
  transition: color .15s ease, border-color .15s ease;
}
.award-filter:hover { color: var(--ink); border-color: var(--ink-soft); }
/* The active chip carries full ink and a matching border rather than a
   fill — a filled pill would read as a button you press, not as the state
   the list is already in. */
.award-filter.is-active { color: var(--ink); border-color: var(--ink); }
/* Same icon-under-image stack .cred-logo uses on the cards: the icon is
   always in the markup, an uploaded logo layers over it, and onerror
   strips a broken one so the icon shows through again. */
.award-filter-logo {
  position: relative;
  flex-shrink: 0;
  width: 18px;
  height: 18px;
  display: flex;
  align-items: center;
  justify-content: center;
  border-radius: 4px;
  overflow: hidden;
}
.award-filter-logo svg { width: 14px; height: 14px; }
.award-filter-logo img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: contain;
}
.award-empty { font-size: 14px; }
/* ---- Awards: the placement badge ----
   The logo slot carries the RESULT on this tab, not a generic trophy —
   Placement itself lives inside the collapsed panel, so this is what puts
   the outcome on the card face. Short codes only (1, 2, QF, R16, Dean):
   a 42px tile can't hold "Group Stage" legibly, so awards.js maps the
   long forms down before they get here. The longest code that reaches
   here is three characters ("SGH", "R32"), which fits at 13px with room
   to spare — nothing needs to shrink to fit.
   Deliberately does NOT touch width/height/border-radius: it keeps
   .cred-logo's own 42px rounded square, so the badge sits in exactly the
   footprint the icon did and stays in line with every other tab's logo
   slot. A full circle was tried and reverted — it read as a different,
   smaller element than the tile it replaced.
   Colours are declared as a local --tone/--tone-soft pair per modifier,
   so the shared rule below stays one block instead of six near-identical
   ones. */
#awards-list .cred-badge {
  background: var(--tone-soft);
  border-color: transparent;
  /* No ring and no solid fill. Both were tried and read as too heavy —
     the tile is only 42px, so an outline plus a fill plus a numeral is
     three competing elements in a very small box. One soft wash of the
     tone, with the code itself carrying the colour, is enough to code the
     result without the badge becoming the loudest thing on the card. */
  box-shadow: none;
  color: var(--tone);
  font-size: 13px;
  font-weight: 600;
  letter-spacing: -.02em;
}
#awards-list .cred-badge-text { line-height: 1; }
/* The trophy on a 1st-place badge. A touch smaller than .cred-logo's own
   20px icon so it sits at the same optical weight as the 13px codes it
   shares the grid with, and it inherits `color` from the badge, so it
   comes out gold without a rule of its own. */
#awards-list .cred-badge svg { width: 18px; height: 18px; }
/* Fallback tone, so an unmapped placement still renders as a readable
   badge rather than with undefined custom properties. */
/* Each wash is the tone at ~8% — just enough to tint the tile without it
   reading as filled. The dark tone stays fully neutral: a knockout exit
   shouldn't be tinted at all, which is what leaves the podium colours as
   the only chromatic badges on the wall. */
#awards-list .cred-badge { --tone: var(--ink-soft); --tone-soft: var(--bg); }
#awards-list .cred-badge--gold   { --tone: #9a7b16; --tone-soft: rgba(154,123,22,.09); }
#awards-list .cred-badge--silver { --tone: #6f7276; --tone-soft: rgba(111,114,118,.09); }
#awards-list .cred-badge--bronze { --tone: #8a5a2b; --tone-soft: rgba(138,90,43,.09); }
#awards-list .cred-badge--dark   { --tone: var(--ink-soft); --tone-soft: var(--bg); }
#awards-list .cred-badge--green  { --tone: #1a7f43; --tone-soft: rgba(26,127,67,.09); }
#awards-list .cred-badge--blue   { --tone: var(--accent); --tone-soft: var(--accent-soft); }

/* A solid podium fill was tried here and removed — it read as too heavy.
   Hierarchy now comes from COLOUR rather than weight: the podium tones
   are the only chromatic ones, so gold/silver/bronze still catch the eye
   against the neutral greys of the knockout rounds, without any badge
   turning into a block of colour.

   Dark mode: the light tints go muddy on a dark card, so each tone
   brightens and its wash drops to a translucent version of itself. */
[data-theme="dark"] #awards-list .cred-badge--gold   { --tone: #e8c65c; --tone-soft: rgba(232,198,92,.14); }
[data-theme="dark"] #awards-list .cred-badge--silver { --tone: #c6c8cc; --tone-soft: rgba(198,200,204,.14); }
[data-theme="dark"] #awards-list .cred-badge--bronze { --tone: #d29a63; --tone-soft: rgba(210,154,99,.14); }
[data-theme="dark"] #awards-list .cred-badge--green  { --tone: #4ac07c; --tone-soft: rgba(74,192,124,.14); }
/* The shared .cred-head is a 2-column grid so a title can pair with a
   date on its right. Awards has no date up here any more — it moved into
   the detail list — so that second column would just be dead space, and
   the 2fr/1fr split would make a long award title wrap a third earlier
   than it needs to. One column instead, so the title and issuer use the
   card's full width. */
#awards-list .cred-head { grid-template-columns: minmax(0, 1fr); }
/* Containing block for the edge-anchored chevron toggle — same job
   #licenses-list .cred-item does for its own. Without it the button's
   bottom:0 would resolve against the page, not the card. */
#awards-list .cred-item { position: relative; }
/* The wider-photo override for Awards is NOT here — it lives immediately
   after the shared .media-rail-wrap rule further down, because it has to.
   See the note there. */
/* Awards' two-column detail rule is NOT here either — it's shared with
   Projects, further down. A fixed 2-track grid was tried first and had to
   go: it still shared row heights, so a wrapping value left a gap beside
   it. See the note by #projects-list .cred-detail. */
/* Projects briefly overrode align-items to stretch, back when the detail
   block was always fully visible and Modis (demo, everything filled in)
   sat in a row next to teamkyzer.com (still mostly brackets) — start left
   a stray gap of bare page background under the shorter card, up to
   whatever height the tallest neighbour forced the row to. Stretch fixed
   that by letting the shorter card's own box fill the row instead.
   That reasoning stopped holding once the detail block got its own See
   more toggle: stretch ties every card in a GRID ROW to the same height,
   and a grid row's height is recalculated live as its content changes —
   so expanding teamkyzer.com's toggle grew ITS row, and stretch pulled
   Modis (never touched, still collapsed) up to match that new height too,
   even though the two aren't related beyond sharing a row. Same bug
   Licenses hit for the same reason, fixed the same way there. Back to
   start: collapsed cards are close enough in height on their own now
   (the toggle hides exactly the content that used to vary the most), and
   only the card someone actually opens should grow. */
/* The SAME row-sharing problem, one level in — and spanning the tall
   rows full-width (the first fix) solved the gap but left everything in
   one tall, left-heavy stack instead of a genuine balanced 2-column
   layout. The actual fix: stop sharing a grid at all. projects.js's
   detailHTML now renders TWO independent <dl class="cred-detail-col">
   elements (3 facts, 3 lists) inside the outer .cred-detail, instead of
   one shared grid every field auto-placed into. flex, not grid — each
   column is now free to size to its OWN content, so pairing a one-line
   fact with a five-line bullet list on the "same row" can't happen; there
   is no shared row any more, just two columns of genuinely different
   heights sitting side by side.
   Awards shares this for the same reason, found the same way: a long
   value in Representing ("Sekolah Kebangsaan Tanjong Mas") wrapped to two
   lines and stretched its whole grid row, leaving a visible gap under the
   one-line Role beside it. Career/Courses/Events keep the plain grid —
   they don't split .cred-detail into two columns at all. */
#projects-list .cred-detail,
#awards-list .cred-detail {
  display: flex;
  gap: 20px;
}
#projects-list .cred-detail-col,
#awards-list .cred-detail-col {
  flex: 1;
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: 8px;
}
/* Licenses: three per row, not two — these entries are far thinner (a
   title, issuer, date, maybe a credential ID), so 420px-wide cards would
   sit mostly empty. 300px is enough for a realistic certification name to
   wrap without feeling cramped, and fits three across this page's own
   988px content width with room to spare — auto-fit still collapses it to
   fewer columns if that width ever shrinks, no separate breakpoint needed. */
/* align-items: start, NOT stretch. Stretch was the right fix back when
   every card showed all its facts inline and they genuinely differed in
   length (MUET's one line vs CMSRL's three) — matching row height then
   meant no card looked randomly shorter. Now that the facts collapse
   behind the edge-toggle, every card is the SAME height while collapsed
   (same image/identity shape, nothing else visible) regardless of how
   much is hidden inside — so stretch has nothing left to fix, and
   actively breaks the toggle: expanding CMSRL grows ITS grid row, and
   stretch was pulling MUET/the driving licence up to match that new
   height too, leaving empty space in cards nobody opened and dragging
   their edge-toggle down to the wrong spot with it. align-items: start
   lets every card size to its own content again, so only the card that's
   actually open grows. */
#licenses-list {
  display: flex;
  align-items: flex-start;
  gap: 20px;
}
.card.cred-item { border-top: none; padding: 22px 24px; }
.card.cred-item:hover { transform: none; box-shadow: var(--shadow); }
/* Splits the at-a-glance metadata from the reflective/detail content below
   it — Courses and Events use it for provider/role+date vs. media and
   description; Licenses (below) uses it to separate the certificate image,
   the identity row, and the facts into three distinct sections. The
   still-static Skills tab keeps the plain .cred-item row and never sees
   this. */
.cred-divider { border: none; border-top: 1px solid var(--line); margin: 14px 0 2px; width: 100%; }

/* ---- Licenses: image-led card ----
   A deliberately different shape from Courses/Events, which lead with
   metadata because their whole point is the story (what happened, what you
   got out of it). A licence's whole point is the document itself, so the
   card leads with a full-width photo of it, then a compact identity row
   (logo + name + issuer), then the plain facts — each section its own
   .cred-divider-separated block, stacked in a column rather than the
   shared .cred-item's default row (logo beside a body). Scoped to
   #licenses-list only; Skills keeps the original row layout. */
#licenses-list .cred-item {
  display: flex;
  flex-direction: column;
  gap: 14px;
  /* Anchors the edge-toggle button below (position:absolute; bottom:0) to
     THIS box specifically — its containing block — so the button tracks
     the card's own rendered bottom edge rather than the page's. Since that
     edge moves whenever the collapsible panel opens/closes, the button
     "moves along to the bottom of the expanded card" for free: no JS
     reposition needed, just what position:absolute already does against a
     box whose height changes. */
  position: relative;
}
/* width:100% (the base rule above) only fills the CONTENT box — still
   inset from the card's actual edges by the card's own 24px side padding,
   which read as "not reaching the sides" even though it matched the image/
   head width above it. width:auto + matching negative side margins lets it
   bleed straight through that padding to the card's true left/right edges,
   the same trick a full-bleed image or rule uses inside a padded box. Only
   one divider left now (inside the collapsible panel, between the identity
   row and the facts) — the one that used to sit above the identity row was
   removed on request, so a collapsed card ends cleanly right after the
   identity row with nothing separating it from the card's own edge. */
#licenses-list .cred-divider { width: auto; margin: 2px -24px; }
#licenses-list .cred-cert-image {
  position: relative;
  width: 100%;
  aspect-ratio: 2 / 1;
  border-radius: var(--radius-sm);
  background: var(--bg);
  border: 1px solid var(--line);
  overflow: hidden;
  display: flex;
  align-items: center;
  justify-content: center;
  color: var(--ink-soft);
}
#licenses-list .cred-cert-image svg { width: 26px; height: 26px; }
/* Same fallback shape every photo slot on this site uses: the icon sits
   underneath, an empty/broken img is stripped by onerror so the icon shows
   through. contain, not cover — a certificate's own text and signatures
   must never be cropped just to fill the frame. */
#licenses-list .cred-cert-image img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: contain;
  background: var(--surface);
}
/* align-items: flex-start, not center — the logo's top edge lines up with
   the licence name's top edge, on request, rather than the whole logo
   floating centered against the two-line name+issuer stack beside it. */
#licenses-list .cred-cert-head {
  display: flex;
  align-items: flex-start;
  gap: 12px;
}
/* Bigger than the shared 42px .cred-logo, on request — this row has no
   grid/date competing for space beside it the way Courses/Events' header
   does, so the logo can afford to lead more. 56px read as too big, then
   46px still slightly too big — 40px is a smaller step up instead. Icon
   scaled down to match. */
#licenses-list .cred-logo { width: 40px; height: 40px; }
#licenses-list .cred-logo svg { width: 19px; height: 19px; }
/* Deliberately NOT the bold .cred-title look every other card on the site
   uses — smaller and regular weight, on request, so the logo+name+issuer
   row reads as a quiet identity line under the certificate photo rather
   than competing with it. */
#licenses-list .cred-cert-name {
  font-size: 14px;
  font-weight: 400;
  letter-spacing: normal;
  line-height: 1.3;
}
#licenses-list .cred-sub { font-size: 13px; color: var(--ink-soft); line-height: 1.3; }
/* Collapsed-height consistency, WITHOUT align-items: stretch (that's what
   broke the toggle — see the grid rule above). A short name like the
   driving licence's placeholder wraps to one line while MUET/CMSRL's real
   names wrap to two, so left alone the three collapsed cards end at
   slightly different heights. Reserving room for the longest realistic
   case up front — 2 lines of name + 1 line of issuer, no gap between them
   (the sitewide reset zeroes h3/p margins) — makes every card claim that
   same minimum regardless of how short its actual text is. A genuinely
   longer name than that still just grows the card past the minimum,
   same as before; this only pads the short ones up to match. */
#licenses-list .cred-cert-text { min-height: 54px; }

/* The collapsible panel (Issued/Expires/Credential ID/Skills) — hidden by
   default, same display:none/flex pair as every other .timeline-more on
   the site, just reached through .cred-body here. Two-class selector
   still beats .cred-body's own base `display: flex` on specificity
   regardless of source order, same fix as before. */
#licenses-list .cred-body.timeline-more { display: none; }
#licenses-list .cred-body.timeline-more.is-open { display: flex; }
/* The toggle itself: a small circular handle straddling the card's own
   bottom border, on request — not an inline "See more" button living in
   the card's normal content flow. position:absolute + bottom:0 measures
   against the .cred-item's own box (its containing block — both lists set
   position:relative for exactly this), so it always sits at THAT card's
   current bottom edge: short when collapsed, lower down once the panel
   above it pushes the card taller. translate(-50%, 50%) centers it
   horizontally and drops it by half its own height, so it visually
   straddles the border rather than sitting flush inside or outside it.
   Shared with Awards, which asked for the same control rather than its
   own near-identical copy. */
#licenses-list .timeline-toggle.cred-edge-toggle,
#awards-list .timeline-toggle.cred-edge-toggle {
  position: absolute;
  left: 50%;
  bottom: 0;
  transform: translate(-50%, 50%);
  width: 26px;
  height: 26px;
  display: flex;
  align-items: center;
  justify-content: center;
  margin: 0;
  border-radius: 50%;
  background: var(--surface);
  border: 1px solid var(--line);
  box-shadow: var(--shadow);
  color: var(--ink-soft);
}
#licenses-list .timeline-toggle.cred-edge-toggle:hover,
#awards-list .timeline-toggle.cred-edge-toggle:hover { color: var(--ink); }
#licenses-list .timeline-toggle.cred-edge-toggle svg,
#awards-list .timeline-toggle.cred-edge-toggle svg {
  width: 14px;
  height: 14px;
  display: block;
  transition: transform .2s ease;
}
/* Rotating the same chevron-down 180° is what makes it read as pointing
   up — "click to collapse" — rather than swapping to a different icon. */
#licenses-list .timeline-toggle.cred-edge-toggle.is-open svg,
#awards-list .timeline-toggle.cred-edge-toggle.is-open svg { transform: rotate(180deg); }

/* Courses, Events, Projects and Awards stack their card in a COLUMN,
   unlike the shared .cred-item row. The logo moved inside .cred-entry
   (below), so the divider and everything under it are direct children of
   the card and run its full width — exactly how Career/Education work,
   and the reason this had to change: with the logo beside the whole
   card, every row below the header was silently indented past it.
   Scoped to these four lists so the still-static Skills tab keeps the
   original row layout. */
#courses-list .cred-item,
#events-list .cred-item,
#projects-list .cred-item,
#awards-list .cred-item {
  display: flex;
  flex-direction: column;
  gap: 3px;
}
/* The header row itself: logo, then the title/date grid beside it. */
.cred-entry { display: flex; gap: 14px; align-items: flex-start; }
/* A two-column GRID rather than two stacked flex columns. That's the whole
   point: a grid puts left/right pairs on shared rows, so title↔date and
   provider↔location line up by construction. Two independent columns
   can't — the title is 16px and the date 13px, so each column's row 2
   started at a different height and the pairs drifted apart.
   `align-items: baseline` then aligns each pair on its text baseline, and
   the 2fr/1fr split keeps a long venue name wrapping inside its own column
   instead of squeezing the title. */
.cred-head {
  flex: 1;
  min-width: 0;
  display: grid;
  grid-template-columns: minmax(0, 2fr) minmax(0, 1fr);
  column-gap: 12px;
  row-gap: 3px;
  align-items: baseline;
}
.cred-title, .cred-sub { grid-column: 1; }
/* Courses, Events, Projects and Awards only — a smaller headline than the
   shared 16px .cred-title, since these cards already carry a divider,
   media, and a detail section below; the still-static Skills tab keeps
   the original size. */
#courses-list .cred-title,
#events-list .cred-title,
#projects-list .cred-title { font-size: 15px; }
/* Awards drops one step further than its siblings, on request. It sits
   BELOW that rule rather than inside it so the other three tabs keep 15px
   — and below rather than above because the two selectors would otherwise
   have equal specificity and source order alone would decide. */
#awards-list .cred-title { font-size: 14px; }
.cred-date, .cred-place, .cred-dur, .cred-role { grid-column: 2; text-align: right; }
.cred-date { font-size: 13px; color: var(--ink-soft); white-space: nowrap; }
.cred-dur { font-size: 12px; color: var(--ink-soft); white-space: nowrap; }
.cred-place { font-size: 12px; color: var(--ink-soft); }
/* Projects only — Role, paired with Category · Industry on the row below
   Title/Date. Same muted weight as .cred-place, since it's secondary
   metadata beside the title, not a second heading. */
.cred-role { font-size: 12px; color: var(--ink-soft); }
/* The provider/organiser reads as quiet metadata here, same weight as the
   date beside it — not the darker --ink the shared .cred-sub uses on other
   tabs. It's a flex row so a long name can be clipped while the "+N"
   counter beside it stays put: min-width:0 is what actually permits the
   name to shrink below its content width (a flex item won't otherwise), and
   the counter's flex-shrink:0 keeps it from being the thing that gets cut. */
#courses-list .cred-sub,
#events-list .cred-sub {
  color: var(--ink-soft);
  display: flex;
  align-items: baseline;
  gap: 6px;
  min-width: 0;
}
.cred-sub-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; min-width: 0; }
.cred-more { flex-shrink: 0; font-size: 12px; color: var(--ink-soft); cursor: default; }

.career-head {
  display: flex;
  align-items: flex-start;
  gap: 14px;
  padding-bottom: 16px;
  margin-bottom: 16px;
  border-bottom: 1px solid var(--line);
}
.career-logo {
  position: relative;            /* the logo <img> is layered over the icon */
  flex-shrink: 0;
  width: 48px; height: 48px;
  border-radius: 12px;
  background: var(--bg);
  border: 1px solid var(--line);
  display: flex;
  align-items: center;
  justify-content: center;
  color: var(--ink-soft);
  overflow: hidden;              /* clips a real logo to the rounded shape */
}
.career-logo svg { width: 22px; height: 22px; }
/* contain, not cover: a logo must never be cropped — a wide one letterboxes
   instead. White behind it because brand logos are drawn for white, so a
   transparent PNG still reads correctly in dark mode. */
.career-logo img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: contain;
  padding: 5px;
  background: #fff;
}

.career-id { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 3px; }
/* The COMPANY leads the card now — it's the thing the card is about. */
.career-company { font-size: 18px; font-weight: 600; letter-spacing: -0.01em; }
.career-industry { font-size: 13px; color: var(--ink-soft); }
.career-span { font-size: 13px; color: var(--ink-soft); }

/* ---- single-role card (the Rectifier pattern) ----
   When a company only ever had one role there is nothing to group, so the
   role sits in the header and the dates are stated ONCE, right-aligned —
   the way they read on a CV.

   ONE bold thing per card. The role is the headline, so it gets the weight;
   the company drops to regular. Two 600-weight lines stacked read as two
   competing titles and neither wins. */
.is-single .career-role-line {
  font-size: 18px;
  font-weight: 600;
  letter-spacing: -0.01em;
  color: var(--ink);
}
.is-single .career-company {
  font-size: 15px;
  font-weight: 400;
  letter-spacing: 0;
  color: var(--ink);
}
/* Employment type trails the company name, dimmer, so the company still
   reads as the thing and the type as a qualifier on it. */
.career-type { color: var(--ink-soft); }
.career-when {
  flex-shrink: 0;
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  gap: 1px;
  text-align: right;
}
.career-dates { font-size: 13px; color: var(--ink); white-space: nowrap; }
.career-dur { font-size: 12px; color: var(--ink-soft); }
/* Location sits under the duration, with a little air so the three lines
   read as "when / how long / where" rather than one block of grey. */
.career-place { font-size: 12px; color: var(--ink-soft); margin-top: 6px; }

/* A description is prose, so it gets a paragraph — not a one-row <dl>, which
   the auto-fit grid squeezed into a single narrow column.
   Slightly heavier than body text: with the detail behind a toggle, this is
   the one line that has to land if the reader stops here. */
.pos-desc {
  font-size: 15px;
  font-weight: 500;
  line-height: 1.6;
  color: var(--ink);
}
/* "See more" sits BELOW the summary now, not at the end of its last line —
   matching Courses' own toggle placement. .pos-desc is a block-level <p>,
   so once this wrapper stopped forcing them onto one flex row, the button
   (an inline-block by default) naturally starts on its own line beneath it,
   picking up .timeline-toggle's own margin-top for the gap. */
.pos-summary { margin-top: 16px; }

/* Bullets, matching the format on your CV. */
.pos-points {
  list-style: none;
  display: flex;
  flex-direction: column;
  gap: 6px;
  margin-top: 12px;
}
.pos-points li {
  position: relative;
  padding-left: 16px;
  font-size: 14px;
  line-height: 1.55;
  color: var(--ink-soft);
}
.pos-points li::before {
  content: "";
  position: absolute;
  left: 2px;
  top: .62em;
  width: 5px; height: 5px;
  border-radius: 50%;
  background: var(--ink-soft);
}
.is-single .timeline-toggle { margin-top: 14px; }

/* A labelled inline row — icon, label, value. ONE component used for Skills,
   Tools and Website, rather than three near-identical rules. Named .pos-row
   and not .pos-skills precisely because it holds more than skills; a class
   named after its first use is a trap for whoever reads it next. */
.pos-row {
  display: flex;
  align-items: baseline;
  gap: 7px;
  flex-wrap: wrap;
  margin-top: 10px;
  font-size: 13px;
  color: var(--ink);
}
.pos-row svg {
  width: 15px; height: 15px;
  color: var(--ink-soft);
  flex-shrink: 0;
  transform: translateY(2px);   /* optical align with the baseline of the text */
}
.pos-row-label { font-weight: 500; }
.pos-row a { color: var(--accent); text-decoration: none; }
.pos-row a:hover { text-decoration: underline; }

/* Once a role has more than three of these rows, one column is a thin ribbon of
   text with half the card empty beside it. .pos-rows splits them into two.
   grid-auto-flow: column against three explicit rows fills DOWN the first column
   and then across — which is what "3 left, 3 right" means. The default row-wise
   flow would interleave them (Skills | Tools / Website | Stack), so the reading
   order would zig-zag instead of giving two clean lists.
   grid-auto-columns is what makes the two columns equal: the columns here are
   implicit, so grid-template-columns has nothing to size. */
.pos-rows {
  display: grid;
  grid-template-rows: repeat(3, auto);
  grid-auto-flow: column;
  grid-auto-columns: minmax(0, 1fr);
  gap: 10px 28px;
  margin-top: 10px;
}
/* The rows space themselves with margin-top when they stand alone; inside the
   grid that fights the gap and makes the two columns sit unevenly. */
.pos-rows .pos-row { margin-top: 0; min-width: 0; }
/* Screen only. In print the page box is ~658px, narrower than this breakpoint,
   so an unscoped query would silently un-column the printed CV — where two
   columns of three are worth the most, since they save a third of the height. */
@media screen and (max-width: 700px) {
  .pos-rows { grid-auto-flow: row; grid-template-rows: none; }
}

/* Achievements: a labelled sub-list, because "what changed" is usually more
   than one line and deserves its own heading next to the duty bullets. */
.pos-block { margin-top: 16px; }
.pos-block-title {
  font-size: 12px;
  font-weight: 500;
  text-transform: uppercase;
  letter-spacing: 0.05em;
  color: var(--ink-soft);
  margin-bottom: 6px;
}
.pos-block .pos-points { margin-top: 0; }

.pos-status { font-size: 12px; color: var(--ink-soft); margin-top: 14px; }

/* ---- positions inside a company ----
   The left rule ties them together visually: everything indented past that
   line happened at the employer named above it. */
.pos-list {
  list-style: none;
  display: flex;
  flex-direction: column;
  gap: 22px;
}
.pos-item {
  padding-left: 16px;
  border-left: 2px solid var(--line);
}
/* Single-role cards keep the .pos-item wrapper (it's what the Show more
   button looks up to find its block) but lose the grouping rule and indent —
   there is no group of one, and the role already lives in the header. */
.is-single .pos-item { padding-left: 0; border-left: none; }
.is-single .pos-list { gap: 0; }

/* ---- positions as nested cards ----
   On a MULTI-role company card each position becomes its own card, so two
   jobs at one employer read as two distinct things rather than two blocks
   sharing a rule.

   Kept WHITE, not grey. White on white works as long as something else does
   the separating: a hairline border plus a very soft lift is enough, and it
   keeps the nested card feeling like part of the same surface rather than a
   sunken panel. */
.career-card:not(.is-single) .pos-list { gap: 14px; }
.career-card:not(.is-single) .pos-item {
  padding: 18px 20px;
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  background: var(--surface);
  box-shadow: 0 1px 2px rgba(0, 0, 0, .04);
}
/* Dark mode has no white to fall back on — --surface IS the parent card's
   colour, so the border would be doing all the work. A barely-there white
   wash lifts the nested card a step above its parent instead. */
[data-theme="dark"] .career-card:not(.is-single) .pos-item {
  background: rgba(255, 255, 255, .035);
  box-shadow: none;
}
.pos-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 16px;
  flex-wrap: wrap;
}
/* Left column of the position head: role, then employment type. Same job as
   .career-id one level up, so the two heads gap identically. */
.pos-id { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 3px; }
.pos-role { font-size: 16px; font-weight: 600; letter-spacing: -0.01em; }
.pos-dates { font-size: 13px; color: var(--ink-soft); white-space: nowrap; }
/* When / where, stacked on the right of the position head — the same
   arrangement as .career-when on the company header, one level down. */
.pos-when {
  flex-shrink: 0;
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  gap: 1px;
  text-align: right;
}
.pos-place { font-size: 12px; color: var(--ink-soft); }
/* No margin-top: the .pos-id column's own gap handles the spacing, so the
   role→type gap matches the company card's name→industry gap exactly. */
.pos-meta { font-size: 13px; color: var(--ink-soft); }
/* "Current" pill, so the role you're in now reads at a glance */
.career-status {
  flex-shrink: 0;
  font-size: 12px;
  font-weight: 500;
  padding: 4px 11px;
  border-radius: 980px;
  background: var(--accent-soft);
  color: var(--accent);
}

/* Career once nested a Projects / Publications block inside a role, with its
   own heading and rule. Nothing does that any more — a project's facts read as
   .pos-rows alongside Skills and Tools — so .career-sub, .career-sub-title and
   .career-sub .cred-title are all gone rather than left behind as dead rules.
   The .cred-* classes themselves are still very much alive on the Awards,
   Courses, Events, Licenses and Hobbies tabs. */

@media (max-width: 560px) {
  .career-head { flex-wrap: wrap; }
  .career-status { order: -1; }
  /* right-aligned dates don't work once the header stacks */
  .career-when,
  .pos-when { align-items: flex-start; text-align: left; }
}

/* ---- hobbies: a 4-across card grid ----
   Four explicit columns, stepping down as the screen narrows. Each hobby is
   a tile rather than a list row, because these are things you keep and tend
   — they read better side by side than stacked. */
/* A hobby card is three bands: name on white, picture in the middle,
   summary on white. The card itself has no padding so the picture can
   run edge to edge; the two white bands carry their own. */
.hobby-card {
  display: flex;
  flex-direction: column;
  padding: 0;
  overflow: hidden;                /* clips the picture to the rounded corner */
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  background: var(--surface);
  min-width: 0;
}
.hobby-head {
  display: flex;
  align-items: center;
  gap: 9px;
  padding: 12px 14px;
  min-width: 0;                    /* lets the name ellipsis instead of pushing out */
}
/* No tile, no fill, no accent colour. A filled chip is how the Awards
   badges mark a RESULT; a hobby icon only identifies, so it sits as bare
   line-art in the same muted ink as the summary below. The card then has
   exactly one thing competing for attention: the picture. */
.hobby-logo {
  flex: none;
  display: flex;
  align-items: center;
  color: var(--ink-soft);
}
.hobby-logo svg {
  width: 16px;
  height: 16px;
  /* Lighter than the default so it reads as a mark beside the name
     rather than a second heading next to it. */
  stroke-width: 1.75;
}
.hobby-name {
  font-size: 14px;
  font-weight: 600;
  letter-spacing: -0.01em;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
/* The picture band. 4:3 to match every other photo frame on the site.
   The neutral image icon is the empty state — the hobby's OWN icon is
   already up in the head, so repeating it here would say it twice. */
.hobby-cover {
  position: relative;
  aspect-ratio: 4 / 3;
  display: flex;
  align-items: center;
  justify-content: center;
  background: var(--bg);
  color: var(--ink-soft);
  border-top: 1px solid var(--line);
  border-bottom: 1px solid var(--line);
}
.hobby-cover svg { width: 24px; height: 24px; }
/* Absolute + cover so a photo of any shape fills the band rather than
   letterboxing inside it. Removes itself if the file is missing, which
   drops back to the icon underneath. */
.hobby-cover img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
}
.hobby-summary {
  padding: 11px 14px 13px;
  font-size: 12px;
  color: var(--ink-soft);
  line-height: 1.55;
}

/* ---- media rail: three photos per row, arrows either side ----
   The arrows sit OUTSIDE the rail rather than floating over the photos, so
   they never cover the thing you're trying to look at. Flex handles the
   vertical centring, which means no magic offset numbers to maintain. */
.media-rail-wrap {
  display: flex;
  align-items: center;
  gap: 8px;
  margin-top: 16px;
}
/* Grid with column flow = a row of equal cards. Each is a third of the rail
   minus the two gaps, so exactly three fit and the fourth peeks in. */
.media-rail {
  flex: 1;
  min-width: 0;
  list-style: none;
  display: grid;
  grid-auto-flow: column;
  grid-auto-columns: calc((100% - 2 * 12px) / 3);
  gap: 12px;
  overflow-x: auto;
  scroll-snap-type: x mandatory;   /* cards land square, never half-shown */
  scroll-behavior: smooth;
  scrollbar-width: none;           /* the arrows are the control here */
  padding-bottom: 2px;             /* room for a focus ring */
}
.media-rail::-webkit-scrollbar { display: none; }
.media-card { scroll-snap-align: start; min-width: 0; }
/* Just the photo — no caption stack under it. The description lives in the
   img's alt and the link's aria-label, where it helps a screen reader without
   turning a clean row of tiles into a list of grey labels. */
.media-card a { display: block; text-decoration: none; color: inherit; }

/* The photo itself. cover (not contain) because a photo should fill its
   frame — the opposite of a logo, which must never be cropped. */
.media-frame {
  position: relative;
  aspect-ratio: 4 / 3;
  border-radius: var(--radius-sm);
  border: 1px solid var(--line);
  background: var(--bg);
  display: flex;
  align-items: center;
  justify-content: center;
  color: var(--ink-soft);
  overflow: hidden;
  transition: border-color .15s ease;
}
.media-card a:hover .media-frame { border-color: var(--ink-soft); }
.media-frame svg { width: 22px; height: 22px; }
/* Layered over the icon, and removes itself if the file is missing — same
   fallback trick as the company logos. */
.media-frame img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
}
/* (.media-caption removed — the rail shows photos only. The old flat
   .media-list chips elsewhere on the page still use .media-title.) */

/* Courses and Events reuse the rail markup above, but show ONE photo per
   view rather than Education's three across — these cards are half-width,
   so a third of one would be a thumbnail. The ID selectors are what beat
   the plain .media-rail rules (including the responsive ones below)
   without touching them.

   Two rules learned the hard way, both still true here:

   1. The photo must track the CARD's width, not a constant. It used to be
      pinned to a flat 340px, sized to match Education's per-tile width — but
      Education's cards are full-width with three tiles across, while
      Courses' cards are already half-width with one tile, so a flat 340px
      sat well short of the card's real edges. `.cred-item` is a flex column
      with the default `align-items: stretch`, so leaving `.media-rail-wrap`
      alone (no `fit-content` + auto-margins, which is what disabled stretch
      and caused the original collapse-to-a-pill bug) lets it inherit the
      card's own content width for free — a real, definite length the
      browser already computed, not a guess.
   2. The photo's size must NOT depend on whether the arrows are showing.
      They used to be in-flow flex siblings of the rail, sharing its width
      budget — so a card with arrows rendered a visibly smaller photo than
      one without (the same bug, smaller form, would come right back if the
      rail just flex-grew alongside them here). Fixed by reserving the
      arrows' space as padding on the wrap — constant whether or not they're
      actually visible — and taking them out of flow with
      `position: absolute`, so the rail's width only ever depends on the
      wrap, never on the arrows next to it. */
#courses-list .media-rail-wrap,
#events-list .media-rail-wrap,
#projects-list .media-rail-wrap,
#awards-list .media-rail-wrap {
  position: relative;
  justify-content: center;
  padding: 0 38px; /* 30px button + 8px gap, reserved on both sides whether
                       or not the arrows are actually visible */
}
/* A wider photo on Awards, on request. That 38px each side is fine on a
   420px Courses/Events card, but Awards' are 300px — 76px of reserved
   gutter was eating a quarter of the card and squeezing the photo to
   roughly 175px. Awards still wants the two properties above (the arrows
   are positioned against this wrap), so it stays in that selector list
   and only the padding is overridden.
   THIS RULE MUST STAY BELOW THAT ONE. Both are one ID + one class, so
   they have identical specificity and source order alone decides the
   winner — written higher up in the Awards block, this silently lost and
   the photo never actually widened. */
#awards-list .media-rail-wrap { padding: 0; }

#courses-list .media-rail,
#events-list .media-rail,
#projects-list .media-rail,
#awards-list .media-rail { width: 100%; max-width: 480px; grid-auto-columns: 100%; }
#courses-list .media-nav[data-media-prev],
#courses-list .media-nav[data-media-next],
#events-list .media-nav[data-media-prev],
#events-list .media-nav[data-media-next],
#projects-list .media-nav[data-media-prev],
#projects-list .media-nav[data-media-next],
#awards-list .media-nav[data-media-prev],
#awards-list .media-nav[data-media-next] {
  position: absolute;
  top: 50%;
  transform: translateY(-50%);
}
#courses-list .media-nav[data-media-prev],
#events-list .media-nav[data-media-prev],
#projects-list .media-nav[data-media-prev],
#awards-list .media-nav[data-media-prev] { left: 0; }
#courses-list .media-nav[data-media-next],
#events-list .media-nav[data-media-next],
#projects-list .media-nav[data-media-next],
#awards-list .media-nav[data-media-next] { right: 0; }

/* Skills gained / Highlights (Courses), Attendees / Organisers / Highlights
   (Events), Role / Stack / Status (Projects) and Category / Role / Scope
   (Awards): same treatment Education's own field list gets
   (.edu-feature-detail dt) — a bolder, full-ink label reads as a heading
   over its list rather than a same-weight caption beside a value. Scoped
   to these four lists only, the same way Education's own version is
   scoped to .edu-feature-detail rather than every .timeline-detail: the
   still-static Skills tab keeps the plain .cred-detail styling, and this
   shouldn't bold that too. A plain descendant selector, so it reaches
   Events' nested Organisers/Attendees dt just as well as a top-level one. */
#courses-list .cred-detail dt,
#events-list .cred-detail dt,
#projects-list .cred-detail dt,
#awards-list .cred-detail dt { font-weight: 600; color: var(--ink); }
/* .pos-points assumes it follows a paragraph (margin-top: 12px) — as a dd's
   only content, right under a label, that gap reads as too far. Zeroed for
   the same reason Education zeroes it in the same spot. */
#courses-list .cred-detail dd .pos-points,
#events-list .cred-detail dd .pos-points { margin-top: 0; }
/* Events only: Organisers and Attendees stack together as ONE grid item in
   the right column, rather than Attendees wrapping onto its own row below
   Highlights the way three flat rows would auto-place. Same row-gap as the
   grid itself, so the stacked pair reads as part of the same rhythm. */
#events-list .cred-detail-stack {
  display: flex;
  flex-direction: column;
  gap: 8px;
}

/* Arrows. Hidden by media.js when everything already fits, and disabled at
   whichever end you've reached. */
.media-nav {
  flex-shrink: 0;
  width: 30px; height: 30px;
  border-radius: 50%;
  border: 1px solid var(--line);
  background: var(--surface);
  color: var(--ink);
  display: flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
  transition: background .15s ease, opacity .15s ease;
}
/* MUST come after the display:flex above.
   The browser's own stylesheet has [hidden] { display: none }, but that's the
   same specificity as .media-nav — and author CSS beats the browser's. So
   `display: flex` silently won and the hidden attribute did nothing: media.js
   was setting hidden correctly and the arrows stayed on screen anyway.
   Any time you give a class `display`, you owe it a [hidden] escape hatch. */
.media-nav[hidden] { display: none; }
.media-nav:hover:not(:disabled) { background: var(--bg); }
.media-nav:disabled { opacity: .3; cursor: default; }
.media-nav svg { width: 16px; height: 16px; }

@media (max-width: 760px) {
  .media-rail { grid-auto-columns: calc((100% - 12px) / 2); }   /* two across */
}
@media (max-width: 460px) {
  .media-rail { grid-auto-columns: 100%; }                      /* one across */
}

/* ---- GPA / semester breakdown ---- */
.gpa-block { margin-top: 20px; }
.gpa-summary {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 12px;
  padding: 16px 18px;
  border-radius: var(--radius-sm);
  background: var(--accent-soft);
  margin-bottom: 14px;
}
.gpa-stat { display: flex; flex-direction: column; gap: 2px; text-align: center; }
.gpa-label { font-size: 12px; color: var(--ink-soft); }
.gpa-value {
  font-size: 22px;
  font-weight: 600;
  letter-spacing: -0.01em;
  font-variant-numeric: tabular-nums;
}

/* The CGPA/Credits/Semester overview, reworked as a .sem-head button — same
   white card, border and chevron as every other result box (UPKK, UPSR,
   PT3, SPM), so clicking it opens/closes the semester breakdown instead of
   the whole thing sitting open by default. Reuses .gpa-stat/.gpa-label/
   .gpa-value as-is, just resized — the original 14px value read too small
   next to "Result" (15px), and the three stats sat too close together at
   20px apart. */
.sem-meta.gpa-overview-meta { display: flex; align-items: center; gap: 36px; }
.gpa-overview-meta .gpa-stat { gap: 2px; }
.gpa-overview-meta .gpa-value { font-size: 16px; letter-spacing: normal; color: var(--ink); }

.sem-list { list-style: none; display: flex; flex-direction: column; gap: 8px; }
.sem { border: 1px solid var(--line); border-radius: var(--radius-sm); overflow: hidden; }
.sem-head {
  display: flex;
  align-items: center;
  gap: 12px;
  width: 100%;
  padding: 12px 14px;
  border: none;
  background: var(--surface);
  font: inherit;
  text-align: left;
  cursor: pointer;
  transition: background .15s ease;
}
.sem-head:hover { background: var(--bg); }
/* .sem-head on an <a> (the Transcript row — a real link out, not a toggle):
   reset the anchor defaults so it reads identically to every other row. */
a.sem-head { text-decoration: none; color: inherit; }
.sem-name { font-size: 15px; font-weight: 500; flex: 1; }
/* Semester bars nested inside another .sem's body (Bachelor's "Result"
   overview — Semester 1-6, and the Transcript row alongside them) drop the
   500-weight name down to regular, on request. Scoped to nested lists only:
   the OUTER "Result" bar's own name isn't inside a .sem-body, so it keeps
   its weight, and neither does Diploma's un-nested semester list. */
.sem-body > .sem-list .sem-name { font-weight: 400; }
/* Transcript is the one exception in that same nested list, on request —
   bold again. It's the only row using <a class="sem-head"> instead of
   <button>, so that's a real structural hook, not a new one-off class:
   higher specificity (a type selector added) beats the rule above
   regardless of source order. */
.sem-body > .sem-list a.sem-head .sem-name { font-weight: 600; }
.sem-meta {
  font-size: 13px;
  color: var(--ink-soft);
  font-variant-numeric: tabular-nums;
}
/* The Transcript row's PDF label — same bold-accent treatment as every
   other document link on the page (.exam-slip-link), just as a plain span
   since the whole row is already the <a>; nesting a link inside a link
   isn't valid HTML. */
.sem-meta.sem-meta--pdf {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  color: var(--accent);
  font-weight: 600;
}
.sem-meta--pdf svg { width: 14px; height: 14px; }
.sem-meta strong { color: var(--ink); }
.sem-chevron {
  width: 16px; height: 16px;
  color: var(--ink-soft);
  flex-shrink: 0;
  transition: transform .2s ease;
}
/* Direct-child combinator, not a descendant selector — .sem can now nest
   inside another .sem (Bachelor's "Result" overview wrapping Semester 1/2),
   and a plain descendant selector would match EVERY .sem-chevron/.sem-body
   below an open .sem, not just its own. That silently forced every inner
   semester open (and rotated their chevrons) the moment the outer "Result"
   bar was opened, regardless of the inner .sem's own is-open state. */
.sem.is-open > .sem-head .sem-chevron { transform: rotate(180deg); }

.sem-body { display: none; border-top: 1px solid var(--line); overflow-x: auto; }
.sem.is-open > .sem-body { display: block; }

/* A semester list nested inside another .sem's body (Bachelor's "Result"
   overview) reads as one cohesive block, not 6 separate floating cards.
   First attempt gave the nested items their OWN border + radius on top of
   the outer "Result" .sem's own border + radius — the two competed at the
   seams: doubled-up border thickness (each nested item's left/right/bottom
   border sitting flush against the outer box's own border on those same
   sides), and an inconsistent extra rounded corner where the nested list's
   own first-child radius fought the outer box's already-rounded, already-
   clipping (overflow:hidden) shape.

   Fixed by giving nested items NO border of their own at all, only a
   border-bottom to divide one from the next — and none on the last item,
   since the outer .sem's own border already frames the whole thing on
   every side. No radius anywhere in here either: the outer .sem's
   border-radius + overflow:hidden already clips the entire body (however
   square its contents are) to match its own single rounded shape, so nothing
   inside needs to duplicate that rounding. Scoped to nested lists only —
   Diploma's always-visible semester list, and the UPKK/UPSR/PT3/SPM pairs,
   aren't inside a .sem-body, so they're untouched. */
.sem-body > .sem-list { gap: 0; }
.sem-body > .sem-list > .sem {
  border: none;
  border-radius: 0;
  border-bottom: 1px solid var(--line);
}
.sem-body > .sem-list > .sem:last-child { border-bottom: none; }

.sem-table { width: 100%; border-collapse: collapse; font-size: 13px; min-width: 460px; }
.sem-table th {
  text-align: left;
  font-weight: 400;
  font-size: 12px;
  color: var(--ink-soft);
  padding: 9px 14px;
  border-bottom: 1px solid var(--line);
  white-space: nowrap;
}
.sem-table td {
  padding: 9px 14px;
  border-bottom: 1px solid var(--line);
}
.sem-table tr:last-child td { border-bottom: none; }
/* Numbers in columns get tabular figures so the digits line up. */
.sem-cr, .sem-grade, .sem-points { font-variant-numeric: tabular-nums; white-space: nowrap; }
.sem-code { font-size: 12px; color: var(--ink-soft); white-space: nowrap; }
.sem-grade { font-weight: 600; }
/* Subject name → its own page (not built yet, hence href="#"). Deliberately
   NOT styled like a normal link (no accent color, no permanent underline) —
   plain black text that only reveals it's clickable on hover, so the table
   doesn't read as a wall of blue links. */
.sem-subject-link { color: inherit; text-decoration: none; }
.sem-subject-link:hover { text-decoration: underline; }
.sem-table th:nth-child(3), .sem-table td:nth-child(3),
.sem-table th:nth-child(4), .sem-table td:nth-child(4),
.sem-table th:nth-child(5), .sem-table td:nth-child(5) { text-align: right; }

/* Two-column Subject/Grade tables (UPKK, UPSR) don't need the 460px floor
   that the 5-column university table forces — that width was only there to
   keep Code/Cr/Grade/Points from crowding, and would just add a pointless
   scrollbar to a much narrower box. */
.sem-table.exam-table { min-width: 0; }
.sem-table.exam-table th:last-child, .sem-table.exam-table td:last-child { text-align: right; }

/* Side-by-side pair of .sem boxes — UPKK and UPSR, one exam per column
   instead of one row per semester like the university GPA blocks use. */
.sem-list.sem-list--pair { flex-direction: row; align-items: flex-start; }
.sem-list.sem-list--pair > .sem { flex: 1 1 0; min-width: 0; }
@media (max-width: 560px) {
  .sem-list.sem-list--pair { flex-direction: column; }
}

/* The result slip is its own row in the Subject/Grade table now, not a
   separate .media-list block — same two columns, so it reads as part of the
   same list instead of a bolted-on extra underneath it. A top border marks
   it as a different kind of row (a document, not a grade); tr:last-child's
   border-bottom:none already strips its own bottom edge like every other
   last row. */
.exam-slip-row td { border-top: 1px solid var(--line); }
.exam-slip-link {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  color: var(--accent);
  text-decoration: none;
  font-weight: 600;
}
.exam-slip-link:hover { text-decoration: underline; }
.exam-slip-link svg { width: 14px; height: 14px; }

/* Name/result weight swapped for THIS pair only — the university semester
   rows ("Semester 1", "5 cr · GPA 3.67") keep .sem-name/.sem-meta untouched.
   Here the exam name was reading too big and bold next to the card's other
   labels, and the result (8A, 3A 2B) is the fact that should carry weight,
   not the name — so the label drops down, the result goes bold and full ink. */
.sem-list--pair .sem-name { font-size: 13px; font-weight: 400; color: var(--ink); }
.sem-list--pair .sem-meta { font-size: 14px; font-weight: 600; color: var(--ink); }

/* A bit more breathing room in the collapsed head, scoped to this pair only —
   the university semester heads (.sem-head on its own) keep their original
   12px vertical padding. */
.sem-list--pair .sem-head { padding-top: 16px; padding-bottom: 16px; }

/* Small bold heading above the pair, matching the weight of the Leadership/
   Sport/Activities labels right above it in the same feature grid. */
.edu-exam-label { font-size: 14px; font-weight: 600; color: var(--ink); margin: 14px 0 4px; }

.gpa-scale {
  font-size: 11px;
  color: var(--ink-soft);
  margin-top: 10px;
  line-height: 1.6;
}

@media (max-width: 600px) {
  .gpa-summary { grid-template-columns: 1fr; gap: 10px; }
  .gpa-stat { flex-direction: row; justify-content: space-between; text-align: left; }
}

.wt-tabs {
  display: flex;
  gap: 6px;
  border-bottom: 1px solid var(--line);
  margin-bottom: 22px;
  overflow-x: auto;
}
.wt-tab {
  appearance: none;
  background: none;
  border: none;
  border-bottom: 2px solid transparent;
  padding: 10px 14px;
  font: inherit;
  font-size: 14px;
  font-weight: 500;
  color: var(--ink-soft);
  cursor: pointer;
  white-space: nowrap;
  transition: color .15s ease, border-color .15s ease;
}
.wt-tab:hover { color: var(--ink); }
.wt-tab.is-active { color: var(--ink); border-bottom-color: var(--accent); }

.wt-panel { display: none; }
.wt-panel.is-active { display: block; animation: fade .3s ease; }

/* ---- shared card shells ---- */
.wt-card { padding: 0; overflow: hidden; }        /* rows supply their own padding */
.wt-card:hover, .wt-card-pad:hover { transform: none; box-shadow: var(--shadow); }
.wt-card-pad { padding: 24px 26px; }

.wt-setup { text-align: center; padding: 34px 26px; }
.wt-setup h3 { margin-bottom: 6px; }
.wt-setup .wt-btn { margin-top: 16px; }

/* ---- hero: start | ring | target ---- */
.wt-hero {
  display: grid;
  grid-template-columns: 1fr auto 1fr;
  align-items: center;
  gap: 18px;
  padding: 30px 26px 26px;
}
.wt-anchor { text-align: center; display: flex; flex-direction: column; gap: 6px; }
.wt-anchor-label {
  font-size: 13px;
  color: var(--accent);
  font-weight: 500;
}
.wt-anchor-val { font-size: 19px; font-weight: 500; }
.wt-anchor-sub { font-size: 13px; color: var(--ink-soft); }

.wt-ring-wrap { position: relative; width: 190px; height: 190px; }
.wt-ring { width: 100%; height: 100%; transform: rotate(-90deg); }  /* start the arc at 12 o'clock */
.wt-ring-track, .wt-ring-fill {
  fill: none;
  stroke-width: 3;
}
.wt-ring-track { stroke: var(--line); }
.wt-ring-fill {
  stroke: var(--accent);
  stroke-linecap: round;
  transition: stroke-dashoffset .6s cubic-bezier(.4, 0, .2, 1);
}
.wt-ring-inner {
  position: absolute;
  inset: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 4px;
}
/* The hero figure — the one number this whole page exists for. */
.wt-current {
  font-size: 44px;
  font-weight: 600;
  letter-spacing: -0.02em;
  line-height: 1;
}
.wt-unit {
  font-size: .52em;
  font-weight: 400;
  color: var(--ink-soft);
  margin-left: 3px;
}

/* ---- the five rating blocks ---- */
.wt-blocks { display: flex; gap: 3px; margin-top: 4px; }
.wt-b {
  width: 15px; height: 13px;
  border-radius: 2px;
  background: var(--line);
}
.wt-b.is-on  { background: #fab219; }   /* status: warning / on pace */
.wt-b.is-bad { background: #d03b3b; }   /* status: critical — you gained */
.wt-h-blocks .wt-b { width: 12px; height: 11px; }

/* ---- rows inside the overview card ---- */
.wt-block {
  padding: 18px 26px;
  border-top: 1px solid var(--line);
}
.wt-block-title {
  text-align: center;
  font-size: 15px;
  font-weight: 500;
  color: var(--accent);
}
.wt-block-cta { padding: 20px 26px 24px; }
.wt-block-eta { text-align: center; display: flex; flex-direction: column; gap: 4px; }
.wt-eta { font-size: 16px; font-weight: 500; }
.wt-eta.is-soft { font-size: 14px; font-weight: 400; color: var(--ink-soft); }

/* ---- meters (progress / time) ---- */
.wt-meter {
  display: grid;
  grid-template-columns: 74px 1fr 72px;
  align-items: center;
  gap: 14px;
}
.wt-meter + .wt-meter { margin-top: 12px; }
.wt-meter-label { font-size: 14px; color: var(--ink-soft); }
.wt-meter-val { font-size: 19px; font-weight: 500; text-align: right; }
.wt-track {
  height: 4px;
  border-radius: 999px;
  background: var(--line);
  overflow: hidden;
}
.wt-fill {
  display: block;
  height: 100%;
  border-radius: 999px;
  background: var(--accent);
  transition: width .6s cubic-bezier(.4, 0, .2, 1);
}
.wt-fill-time { background: var(--ink-soft); }        /* time is not an achievement */
.wt-fill.is-over { background: #d03b3b; }             /* deadline blown */

/* ---- stat rows ---- */
.wt-stats {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 16px;
  text-align: center;
}
.wt-stat { display: flex; flex-direction: column; gap: 3px; }
.wt-stat-label { font-size: 13px; color: var(--ink-soft); }
.wt-stat-val { font-size: 24px; font-weight: 500; letter-spacing: -0.01em; }

.wt-block-bmi {
  display: flex;
  align-items: baseline;
  justify-content: center;
  gap: 10px;
}
.wt-bmi-val { font-size: 26px; font-weight: 500; }
.wt-bmi-class { font-size: 15px; font-weight: 500; }

/* Status colours. Each ALWAYS ships with its text label, so colour
   reinforces the meaning and is never the only signal. */
.tone-good     { color: #0ca30c; }
.tone-warning  { color: #b07800; }   /* darkened for contrast on white */
.tone-serious  { color: #c9622f; }
.tone-critical { color: #d03b3b; }
[data-theme="dark"] .tone-warning { color: #fab219; }
[data-theme="dark"] .tone-serious { color: #ec835a; }

/* ---- buttons ---- */
.wt-btn {
  appearance: none;
  font: inherit;
  font-size: 14px;
  font-weight: 500;
  padding: 10px 18px;
  border-radius: 980px;               /* Apple pill radius */
  border: 1px solid var(--line);
  background: var(--surface);
  color: var(--ink);
  cursor: pointer;
  transition: background .15s ease, opacity .15s ease;
}
.wt-btn:hover { background: var(--bg); }
.wt-btn-primary {
  background: var(--accent);
  border-color: var(--accent);
  color: #fff;
}
.wt-btn-primary:hover { background: var(--accent); opacity: .88; }
.wt-btn-danger { color: #d03b3b; border-color: #d03b3b; background: none; }
.wt-btn-danger:hover { background: rgba(208, 59, 59, .08); }
.wt-btn-wide { width: 100%; padding: 13px; font-size: 15px; }
.wt-btn-sm { padding: 7px 16px; }

.wt-icon-btn {
  appearance: none;
  border: none;
  background: var(--bg);
  color: var(--ink);
  width: 32px; height: 32px;
  border-radius: 50%;
  font-size: 14px;
  cursor: pointer;
}
.wt-icon-btn:hover { background: var(--line); }

/* ---- graph ---- */
.wt-chart-head { margin-bottom: 18px; }
.wt-chart-head h3 { font-size: 17px; font-weight: 600; }
.wt-chart-head .muted { font-size: 13px; margin-top: 2px; }
.wt-chart-wrap { position: relative; width: 100%; }
.wt-chart { width: 100%; display: block; overflow: visible; }
.wt-empty { font-size: 14px; padding: 26px 0; text-align: center; }

/* Chart marks. The colours live HERE, not in the JS — which is why the
   graph re-colours itself in dark mode without being redrawn. */
.wt-grid   { stroke: var(--line); stroke-width: 1; }
.wt-axis   { fill: var(--ink-soft); font-size: 11px; }
.wt-axis-y { text-anchor: end; font-variant-numeric: tabular-nums; }
.wt-line   { fill: none; stroke: var(--accent); stroke-width: 2; stroke-linejoin: round; stroke-linecap: round; }
.wt-area   { fill: var(--accent); opacity: .10; stroke: none; }
.wt-dot      { fill: var(--accent); }
.wt-dot-ring { fill: var(--surface); }        /* the 2px surface ring */
.wt-end-label { fill: var(--ink); font-size: 12px; font-weight: 600; }
/* Target line: dashed AND labelled, so it never leans on hue alone. */
.wt-target-line  { stroke: #0ca30c; stroke-width: 1.5; stroke-dasharray: 5 4; }
.wt-target-label { fill: #0ca30c; font-size: 11px; font-weight: 500; }
.wt-cross { stroke: var(--ink-soft); stroke-width: 1; stroke-dasharray: 3 3; }
.wt-focus { fill: var(--accent); stroke: var(--surface); stroke-width: 2; }
.wt-hit   { fill: transparent; cursor: crosshair; }

.wt-tip {
  position: absolute;
  pointer-events: none;               /* never block the mouse it follows */
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  box-shadow: var(--shadow);
  padding: 8px 11px;
  display: flex;
  flex-direction: column;
  gap: 1px;
  white-space: nowrap;
  z-index: 5;
}
.wt-tip-date  { font-size: 11px; color: var(--ink-soft); }
.wt-tip-val   { font-size: 15px; font-weight: 600; }
.wt-tip-delta { font-size: 12px; }
.wt-tip-delta.is-down, .wt-h-delta.is-down { color: #0ca30c; }
.wt-tip-delta.is-up,   .wt-h-delta.is-up   { color: #d03b3b; }

/* ---- history ---- */
.wt-history { list-style: none; }
.wt-h-row {
  display: grid;
  grid-template-columns: 1fr auto auto auto;
  align-items: center;
  gap: 16px;
  padding: 14px 0;
  border-top: 1px solid var(--line);
}
.wt-h-row:first-child { border-top: none; }
.wt-h-main { display: flex; flex-direction: column; gap: 2px; }
.wt-h-weight { font-size: 19px; font-weight: 600; }
.wt-h-delta { font-size: 13px; }
.wt-h-delta.is-first { color: var(--ink-soft); }
.wt-h-date {
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  gap: 1px;
  font-size: 13px;
}
.wt-h-day { font-size: 12px; color: var(--ink-soft); }
.wt-h-notes {
  grid-column: 1 / -1;
  font-size: 13px;
  color: var(--ink-soft);
  font-style: italic;
}
.wt-h-del {
  appearance: none;
  border: none;
  background: none;
  color: var(--ink-soft);
  font-size: 13px;
  width: 26px; height: 26px;
  border-radius: 50%;
  cursor: pointer;
  opacity: 0;                          /* stays out of the way until hover */
  transition: opacity .15s ease, background .15s ease;
}
.wt-h-row:hover .wt-h-del { opacity: 1; }
.wt-h-del:focus-visible { opacity: 1; }
.wt-h-del:hover { background: rgba(208, 59, 59, .1); color: #d03b3b; }

/* ---- settings form ---- */
.wt-form { display: flex; flex-direction: column; gap: 18px; }
.wt-form-sep { height: 1px; background: var(--line); }
.wt-field-grid {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 16px 24px;
}
.wt-field { display: flex; flex-direction: column; gap: 6px; }
.wt-field-label { font-size: 13px; color: var(--ink-soft); }
.wt-hint { font-size: 12px; color: var(--ink-soft); }

.wt-field input, .wt-field select, .wt-dialog-row input {
  font: inherit;
  font-size: 15px;
  color: var(--ink);
  background: var(--bg);
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  padding: 9px 12px;
  width: 100%;
  transition: border-color .15s ease;
}
.wt-field input:focus, .wt-field select:focus, .wt-dialog-row input:focus {
  outline: none;
  border-color: var(--accent);
}
/* Wrapper so the unit label sits INSIDE the field, Apple-style. */
.wt-input-wrap { position: relative; display: block; }
.wt-input-wrap input { padding-right: 52px; }
.wt-input-unit {
  position: absolute;
  right: 12px; top: 50%;
  transform: translateY(-50%);
  font-size: 13px;
  color: var(--ink-soft);
  pointer-events: none;
}

/* Segmented control (Kilos | Pounds | Stones) */
.wt-seg {
  display: inline-flex;
  background: var(--bg);
  border: 1px solid var(--line);
  border-radius: 980px;
  padding: 3px;
  gap: 2px;
  align-self: flex-start;
}
.wt-seg-btn {
  appearance: none;
  border: none;
  background: none;
  font: inherit;
  font-size: 13px;
  color: var(--ink-soft);
  padding: 7px 16px;
  border-radius: 980px;
  cursor: pointer;
  transition: background .15s ease, color .15s ease;
}
.wt-seg-btn.is-active {
  background: var(--surface);
  color: var(--ink);
  font-weight: 500;
  box-shadow: 0 1px 2px rgba(0, 0, 0, .06);
}
.wt-form-actions { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
.wt-saved { font-size: 13px; color: #0ca30c; }

/* ---- the Enter Weight dialog ---- */
.wt-dialog {
  border: none;
  padding: 0;
  border-radius: var(--radius);
  background: var(--surface);
  color: var(--ink);
  box-shadow: 0 20px 60px rgba(0, 0, 0, .28);
  width: min(440px, calc(100vw - 32px));
}
.wt-dialog::backdrop { background: rgba(0, 0, 0, .35); backdrop-filter: blur(3px); }
.wt-dialog-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  padding: 14px 16px;
  border-bottom: 1px solid var(--line);
}
.wt-dialog-head h3 { font-size: 16px; font-weight: 600; }
.wt-dialog-body { padding: 20px 22px 24px; display: flex; flex-direction: column; gap: 14px; }
.wt-dialog-row { display: flex; flex-direction: column; gap: 6px; }
.wt-dialog-hint { font-size: 12px; }

/* ---- responsive ---- */
@media (max-width: 760px) {
  .wt-hero {
    grid-template-columns: 1fr;
    justify-items: center;
    gap: 22px;
  }
  .wt-anchor { flex-direction: row; align-items: baseline; gap: 10px; justify-content: center; }
  .wt-field-grid { grid-template-columns: 1fr; }
  .wt-meter { grid-template-columns: 64px 1fr 64px; gap: 10px; }
  .wt-stats { gap: 10px; }
  .wt-stat-val { font-size: 20px; }
  .wt-h-row { grid-template-columns: 1fr auto auto; }
  .wt-h-blocks { display: none; }
}

/* ============================================================
   PRINT — the profile page as an A4 CV
   ------------------------------------------------------------
   THE IDEA: paper is a different medium, so it gets its own rules
   instead of forcing the screen layout to compromise. Same HTML,
   two layouts. That's what @media print is for.

   Width is handled by @page below, NOT by a pixel value: A4 is
   210mm wide, so with 18mm margins the text column comes out at
   about 174mm (≈655px) on its own. Hardcoding a px width here is
   the classic mistake — it either overflows the sheet or leaves a
   gap, and it breaks entirely on Letter paper.
   ============================================================ */

@page {
  size: A4;
  margin: 18mm;
}

@media print {
  /* --- 1. Force the light palette.
     Printing dark mode means a solid black rectangle and an empty ink
     cartridge. We re-point the tokens, so every component follows. */
  :root,
  [data-theme="dark"] {
    --bg:          #ffffff;
    --surface:     #ffffff;
    --ink:         #000000;
    --ink-soft:    #444444;
    --line:        #bbbbbb;
    --accent:      #1d1d1f;   /* near-black: hierarchy without colour ink */
    --accent-soft: #f2f2f2;
    --shadow:      none;
  }
  [data-theme="dark"] body { background: #ffffff; }

  body {
    background: #fff;
    font-size: 10.5pt;        /* pt, not px — print's own unit */
    line-height: 1.45;
  }

  /* --- 2. Let the paper set the width. */
  .profile-main,
  .profile-topbar {
    max-width: none;
    padding: 0;
    margin: 0;
    display: block;
  }

  /* --- 3. Hide the screen chrome. None of it means anything on paper. */
  .profile-topbar,
  .profile-tabs,
  .timeline-toggle,
  .read-more-btn,
  .sem-chevron,
  .media-list,
  .skills-note,
  .section-note,
  .sidebar,
  .sidebar-toggle { display: none !important; }

  /* --- 4. Print EVERY tab, not just the one you last clicked.
     This is the part people forget: only .is-active is visible on
     screen, so a naive print gives you one eighth of your CV. */
  .profile-panel { display: block !important; }

  /* .profile-main is a flex column on screen, so `order` can arrange the
     panels into CV order without touching the HTML. */
  .profile-main { display: flex; flex-direction: column; }
  /* Mirrors the tab order exactly, so the printed CV reads in the same
     sequence as the page. Change one, change the other. */
  #tab-profile   { order: 1; }   /* who you are  */
  #tab-career    { order: 2; }   /* career       */
  #tab-education { order: 3; }   /* education    */
  #tab-courses   { order: 4; }
  #tab-events    { order: 5; }
  #tab-projects  { order: 6; }
  #tab-licenses  { order: 7; }
  #tab-awards    { order: 8; }   /* what it earned */
  #tab-skills    { order: 9; }
  #tab-sports    { order: 10; }  /* personal     */
  #tab-hobbies   { order: 11; }

  /* --- 5. Expand everything collapsed. Print what's hidden or half
     the document is missing. */
  .about-text {
    max-height: none !important;
    overflow: visible !important;
  }
  .about-text::after { display: none !important; }   /* kill the fade */
  .timeline-more { display: block !important; }
  .sem-body { display: block !important; }

  /* --- 6. Don't split an entry across two sheets mid-sentence. */
  .card,
  .profile-section,
  .timeline-item,
  .cred-item,
  .sem,
  .skill-group { break-inside: avoid; page-break-inside: avoid; }
  h1, h2, h3 { break-after: avoid; page-break-after: avoid; }

  /* Flatten the cards: borders and shadows are screen furniture. */
  .card {
    box-shadow: none !important;
    border: none;
    border-top: 1px solid var(--line);
    border-radius: 0;
    padding: 12pt 0 0;
    margin: 0;
    transform: none !important;
  }
  .profile-header { border-top: none; padding-top: 0; }
  .profile-main { gap: 10pt; }

  /* --- 7. A printed link isn't clickable, so show where it goes.
     Skipped for mailto:/tel: and for links whose text IS the URL,
     which would otherwise print the same thing twice. */
  a[href^="http"]:not(.cred-link):not([data-id-field])::after {
    content: " (" attr(href) ")";
    font-size: 8.5pt;
    color: var(--ink-soft);
    word-break: break-all;
  }
  a { text-decoration: none; color: var(--ink); }

  /* --- 8. Small typographic fixes for paper. */
  .profile-id h1 { font-size: 20pt; }
  /* #skills-list is listed explicitly because the screen rule that shrinks
     this tab's headings uses an ID, which outranks a class no matter where
     it sits in the file — without naming it here it would keep its 16px on
     paper while every other heading printed at 12pt. Print has one column,
     so the reason for the smaller size doesn't apply here anyway. */
  .profile-section h2, #skills-list h2 { font-size: 12pt; }
  .cred-title, .timeline-title { font-size: 11pt; }
  /* On A4 the column is ~174mm, so the wide-screen 3-column grids would be
     cramped. Fewer, wider columns read better on paper. */
  .info-grid, .timeline-detail { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .cred-detail { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  /* .hobby-grid is gone — hobbies.js deals the covers into .card-col now.
     4 covers across a 174mm sheet would be ~40mm each, so the columns
     collapse to a single stack and each card is kept off a page break. */
  #hobbies-list { display: block; }
  #hobbies-list .card-col { display: block; }
  .hobby-card { border: 1px solid var(--line); break-inside: avoid; page-break-inside: avoid; margin-bottom: 10pt; }
  .profile-photo { width: 70px; height: 70px; }
  .gpa-summary { background: #f2f2f2; }
  .sem-table { min-width: 0; font-size: 9pt; }
  .sem-body { overflow: visible; }
  .skill { border: 1px solid var(--line); background: #fff; }
}


/* Awards' sub-line: organiser (with its logo) on the left, prize won on
   the right, on one baseline under the title.

   This lives at the END of the file on purpose. `.cred-sub` is set at
   line 1499 and `#licenses-list .cred-sub` / `#courses-list .cred-sub`
   further down; an equal-specificity rule placed above any of those would
   lose on source order alone — the exact bug that made the Awards photo
   look un-widened for several rounds. Below everything, it wins. */
#awards-list .cred-sub {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 10px;
}
/* min-width: 0 is what lets the organiser name actually ellipsis: a flex
   item's default min-width is auto, so without this a long organiser
   pushes the prize off the card instead of truncating itself. */
#awards-list .cred-sub-main {
  display: flex;
  align-items: center;
  gap: 6px;
  min-width: 0;
}
/* A circular slot that is always there, filled or not — the same idea as
   the empty photo frame: an outlined placeholder with the image icon
   showing through reads as "a logo goes here", where rendering nothing
   would read as "this line is finished". */
#awards-list .cred-sub-logo {
  position: relative;
  flex: none;
  width: 18px;
  height: 18px;
  border-radius: 50%;
  overflow: hidden;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border: 1px solid var(--line);
  background: var(--bg);
  color: var(--ink-soft);
}
#awards-list .cred-sub-logo svg { width: 10px; height: 10px; }
/* Layered over the icon, and removes itself if the file is missing — the
   same fallback the photo frames and company logos use. */
#awards-list .cred-sub-logo img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
}
#awards-list .cred-sub-name {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
/* flex: none so the prize is never the thing that shrinks — it's short,
   and it's the number the card exists to show. */
#awards-list .cred-sub-prize {
  flex: none;
  color: var(--ink);
  font-variant-numeric: tabular-nums;
}

/* The Awards editor shows only the optional fields an award actually
   uses, each with its own Remove. The button sits at the end of the
   field's own row rather than floating beside it, so a long hint doesn't
   push it out of line. */
.aw-optional { display: flex; align-items: flex-end; gap: 8px; }
.aw-optional .set-field { flex: 1; min-width: 0; }
.aw-field-remove { flex: none; margin-bottom: 6px; }
.aw-add-field { display: flex; align-items: center; gap: 8px; margin-top: 4px; }
.aw-add-field select { max-width: 220px; }

/* The sort control closes the filter bar. margin-left: auto pushes it to
   the right-hand end of whichever row the group chips wrap onto — the bar
   is flex-wrap, so which row that is depends on the window width. */
.award-sort {
  margin-left: auto;
  display: inline-flex;
  align-items: center;
  gap: 8px;
  font-size: 13px;
  color: var(--ink-soft);
}
.award-sort-label { flex: none; }
/* Deliberately quieter than .award-filter: the chips say what you are
   looking at, this only says how it happens to be ordered. */
.award-sort select {
  padding: 6px 10px;
  border: 1px solid var(--line);
  border-radius: 8px;
  background: var(--surface);
  color: var(--ink);
  font: inherit;
  font-size: 13px;
  cursor: pointer;
}
.award-sort select:hover { border-color: var(--ink-soft); }

/* Hobbies' detail editor: label and value side by side with a Remove.
   Both halves are text inputs because the LABEL belongs to the hobby —
   a bonsai has Species, a car wash has Chemistry. */
.hob-detail-row { display: flex; align-items: center; gap: 8px; margin-bottom: 8px; }
.hob-detail-row input { flex: 1; min-width: 0; }
.hob-detail-row .car-mini { flex: none; }

/* Sports is a fixed 3/4 + 1/4 split, not a deal of equal cards — so it
   is a grid with two named tracks rather than CardColumns. The 3fr/1fr
   ratio is the plan's own; minmax(0, …) on both so a long club name
   shrinks its own column instead of pushing the other one off. */
/* 2fr / 1fr rather than the plan's 3fr / 1fr: at a quarter of a 988px
   page the career card was ~230px, and a real club name plus two number
   columns wants more than that. A third gives it ~310px — enough that
   "Kelantan Darul Naim FC" reads without truncating. */
#sports-list {
  display: grid;
  grid-template-columns: minmax(0, 2fr) minmax(0, 1fr);
  align-items: start;
  gap: 20px;
}
@media (max-width: 860px) { #sports-list { grid-template-columns: minmax(0, 1fr); } }

/* The player card is defined further down, with the coloured banner.
   An earlier version lived here and was removed: its .sport-player-detail
   carried a border-top, which survived the redesign as a stray rule
   across the top of the fact grid. */

/* ---- the career list (right) ---- */
.sport-career { padding: 16px 0 8px; }
.sport-career:hover { transform: none; box-shadow: var(--shadow); }
/* Just a heading now — the appearance and goal columns it used to sit
   opposite are gone, so it no longer needs space-between. */
/* Full-bleed rule under the heading, matching the dividers between rows.
   border-bottom on the head rather than border-top on the first row: the
   line belongs to the heading and should show even when the list below
   is empty. */
.sport-career-head {
  padding: 0 16px 12px;
  border-bottom: 1px solid var(--line);
}
.sport-career-title { font-size: 13px; font-weight: 600; }
.sport-career-list { list-style: none; }
/* ---- a club with a single spell: the flat row it was before ---- */
.sport-row {
  display: flex;
  align-items: flex-start;
  gap: 10px;
  padding: 13px 16px;
}
/* flex: 1 + min-width: 0 so the club name truncates, never the years —
   a flex item defaults to min-width: auto and would otherwise push
   them off the card. */
.sport-row-text {
  flex: 1;
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: 3px;
  line-height: 1.4;
}
.sport-row-club {
  min-width: 0;
  font-size: 13px;
  font-weight: 600;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.sport-row-pos {
  min-width: 0;
  font-size: 11px;
  color: var(--ink-soft);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.sport-row-when {
  flex: none;
  font-size: 11px;
  color: var(--ink-soft);
  white-space: nowrap;
  font-variant-numeric: tabular-nums;
}
/* ---- one block per club, spells nested under it ---- */
.sport-club + .sport-club { border-top: 1px solid var(--line); }
.sport-club-head {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 13px 16px 2px;
}
.sport-club-name {
  min-width: 0;
  font-size: 13px;
  font-weight: 600;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.sport-crest {
  position: relative;
  flex: none;
  width: 28px; height: 28px;
  border-radius: 6px;
  overflow: hidden;
  display: flex;
  align-items: center;
  justify-content: center;
  background: var(--bg);
  color: var(--ink-soft);
}
.sport-crest svg { width: 16px; height: 16px; }
/* contain, not cover: a crest has meaningful edges and must not be
   cropped square the way a photo can be. */
.sport-crest img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: contain; }

/* Indented to clear the crest, so the spells line up under the club
   NAME rather than under its badge. 16 + 22 + 9 = 47px.
   A spell carries .sport-row too, so it inherits the same two-line
   name-over-position shape a flat row has — only the crest differs. */
/* 16 padding + 28 crest + 10 gap = 54, so a spell name lines up under
   the club name in the header above it. */
.sport-spells { list-style: none; padding: 0 16px 10px 54px; }
.sport-spell { padding: 7px 0; }
.sport-career-empty { padding: 12px 16px 8px; font-size: 12px; }

/* ---- career rows are buttons now: clicking one swaps the left panel ---- */
.sport-row {
  width: 100%;
  border: 0;
  background: none;
  font: inherit;
  color: inherit;
  text-align: left;
  cursor: pointer;
  border-radius: 6px;
  transition: background-color .12s ease;
}
.sport-row:hover { background: var(--bg); }
/* The selected row is marked by a left rule rather than a fill — a fill
   would compete with the crest and the card's own surface. */
.sport-row.is-selected { background: var(--accent-soft); }
.sport-row.is-selected .sport-row-club { color: var(--accent); }
.sport-row:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }
/* Nested spells sit inside the indent, so their hover box must not run
   under the club header's crest column. */
.sport-spell .sport-row { padding: 7px 8px; margin-left: -8px; }
/* A spell name sits one level below the club header, so it reads one
   step smaller. Only INSIDE a group — a flat row is the top level for
   its own club and keeps the full 13px.

   Two classes deep, so this beats the plain .sport-row-club on
   specificity rather than on source order — no ordering trap here. */
.sport-spell .sport-row-club { font-size: 12px; }

/* The spell panel is the SAME card as the overall profile — your face,
   your name — so it needs no variant of its own. */
.sport-spell-empty { margin-top: 16px; font-size: 12px; line-height: 1.55; }

/* ---- the spell card: a coloured band, then a grid of facts ---- */
.sport-player { padding: 0; overflow: hidden; }
.sport-player:hover { transform: none; box-shadow: var(--shadow); }
/* The band's background and text colour are set inline from the club's
   own colour, so only the geometry lives here. */
.sport-player-head {
  position: relative;
  display: flex;
  align-items: center;
  gap: 14px;
  padding: 18px 22px;
  background: var(--ink);
  color: #fff;
}
/* Only a NEAR-WHITE band needs a hairline — without one it has no
   visible edge against the card. A saturated colour is its own edge, so
   ruling under it would just be a line nobody asked for. */
.sport-player-head.needs-edge { box-shadow: inset 0 -1px 0 var(--line); }
.sport-player-photo {
  position: relative;
  flex: none;
  width: 62px; height: 62px;
  border-radius: 50%;
  overflow: hidden;
  display: flex;
  align-items: center;
  justify-content: center;
  background: rgba(255,255,255,.14);
  color: inherit;
}
.sport-player-head.is-light .sport-player-photo { background: rgba(0,0,0,.07); }
.sport-player-photo svg { width: 26px; height: 26px; }
.sport-player-photo img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; }
.sport-player-id { min-width: 0; }
.sport-player-name { font-size: 20px; font-weight: 700; letter-spacing: -0.02em; }
.sport-player-role {
  display: flex;
  align-items: center;
  gap: 7px;
  margin-top: 3px;
  font-size: 13px;
  opacity: .82;
}
.sport-player-crest {
  position: relative;
  flex: none;
  width: 17px; height: 17px;
  display: flex;
  align-items: center;
  justify-content: center;
}
.sport-player-crest svg { width: 15px; height: 15px; }
/* Bottom-right of the banner, quiet. Absolutely positioned rather than
   pushed with margin-left: auto, so it pins to the CORNER instead of
   riding the vertical centre alongside the photo — and so its size can
   change without ever nudging the name beside it.

   Small and half-opaque on purpose: a shirt number is a detail, not a
   headline. At 40px and full weight it was competing with your name. */
.sport-player-shirt {
  position: absolute;
  right: 22px;
  bottom: 14px;
  font-size: 20px;
  font-weight: 600;
  line-height: 1;
  letter-spacing: -0.02em;
  opacity: .55;
  font-variant-numeric: tabular-nums;
}
.sport-player-crest img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: contain; }

/* Two columns of VALUE-over-LABEL, each cell ruled underneath — the
   reference's own shape. */
.sport-player-detail {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 0 28px;
  padding: 6px 22px 18px;
}
/* border-TOP, not bottom. A bottom border draws a line after the LAST
   row, which is a rule under nothing; a top border draws between rows
   only. The first row then just needs its own suppressed — and
   nth-child(-n+2) is exact here because the grid is two columns wide,
   whatever the total number of cells. */
.sport-player-detail div { padding: 14px 0; border-top: 1px solid var(--line); }
.sport-player-detail div:nth-child(-n+2) { border-top: 0; }
/* Not bold: these are readings, not headings. The label beneath is
   what tells you which is which, so weight would only add noise. */
.sport-player-detail dd { margin: 0; font-size: 15px; }
.sport-player-detail dt { margin-top: 2px; font-size: 12px; color: var(--ink-soft); }
@media (max-width: 520px) { .sport-player-detail { grid-template-columns: minmax(0, 1fr); } }
.sport-spell-empty { padding: 18px 22px 22px; font-size: 12px; line-height: 1.55; }

/* ---- the left column stacks: profile, then a pair of cards ---- */
.sport-main { display: flex; flex-direction: column; gap: 20px; min-width: 0; }
/* align-items: start so each card takes its OWN height. A grid item
   stretches to fill its row by default, which made the short position
   card stretch to match the club card once the jersey frame made that
   one tall — the same default that had Awards cards matching heights
   across a row. */
.sport-pair {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 20px;
}
@media (max-width: 640px) { .sport-pair { grid-template-columns: minmax(0, 1fr); } }
/* Row 1's right-hand cell: position and tournament, stacked.

   `align-self: start` on the STACK ONLY, rather than `align-items: start`
   on the row, is the whole trick. Stretch is the grid default, so the club
   card grows to whatever row 1's tallest cell is — this stack — while the
   stack itself opts out and stays its natural height. Put the opt-out on
   the row and both cells refuse to grow, which leaves the club card short.
   Put it on neither and the formation card stretches to match the club
   card, which is the bug we fixed earlier.

   It also survives the reverse case: a club card with several jerseys that
   ends up taller sets the row height, and the stack still doesn't stretch. */
.sport-stack { display: flex; flex-direction: column; gap: 20px; min-width: 0; align-self: start; }
.sport-team, .sport-formation, .sport-boots, .sport-gallery { padding: 16px 18px 18px; }
.sport-team:hover, .sport-formation:hover,
.sport-boots:hover, .sport-gallery:hover { transform: none; box-shadow: var(--shadow); }

/* ---- team card ---- */
.sport-team-head { display: flex; align-items: center; gap: 10px; }
.sport-crest--lg { width: 34px; height: 34px; border-radius: 8px; }
.sport-crest--lg svg { width: 18px; height: 18px; }
/* The head is crest + name only now — the dates moved down into the
   meta grid, so there is no second line to stack. */
.sport-team-name {
  min-width: 0;
  font-size: 14px;
  font-weight: 600;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
/* The boots head stacks two lines beside the crest — model, then brand —
   so it needs a column wrapper the club head doesn't. `min-width: 0` is
   what lets the ellipsis on .sport-team-name actually fire inside it; a
   flex child defaults to min-width:auto and refuses to shrink. */
.sport-boots-title { display: flex; flex-direction: column; min-width: 0; }
.sport-boots-brand {
  margin-top: 1px;
  font-size: 11px;
  color: var(--ink-soft);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.sport-team-meta dd { font-variant-numeric: tabular-nums; }
.sport-team-detail { display: flex; flex-direction: column; gap: 10px; margin-top: 14px; }
.sport-team-detail dt { font-size: 11px; color: var(--ink-soft); }
.sport-team-detail dd { margin: 1px 0 0; font-size: 13px; }
.sport-team-empty { margin-top: 14px; font-size: 12px; line-height: 1.55; }

/* ---- formation card ---- */
.sport-formation-body { display: flex; align-items: flex-start; gap: 14px; }
.sport-formation-text { flex: 1; min-width: 0; }
.sport-formation-label { font-size: 12px; font-weight: 600; }
.sport-formation-value { margin-top: 2px; font-size: 12px; color: var(--ink-soft); line-height: 1.45; }
/* The pitch keeps its own aspect so the badge's percentage position lands
   in the same place whatever the card width. */
.sport-pitch {
  position: relative;
  flex: none;
  width: 108px;
  aspect-ratio: 100 / 150;
  border-radius: 6px;
  background: var(--bg);
  overflow: hidden;
}
.sport-pitch-svg { display: block; width: 100%; height: 100%; }
.pitch-line { fill: none; stroke: var(--line); stroke-width: 1.4; }
/* translate(-50%, -50%) so left/top address the badge's CENTRE — without
   it the marker sits down-right of the spot it names. */
.sport-pitch-badge {
  position: absolute;
  transform: translate(-50%, -50%);
  min-width: 26px;
  padding: 3px 5px;
  border-radius: 999px;
  background: var(--accent);
  color: #fff;
  font-size: 10px;
  font-weight: 700;
  line-height: 1;
  text-align: center;
  letter-spacing: .01em;
}

/* Level and Location, side by side on the club card, ruled off from the
   crest and name above. padding-top pairs with the border so the line
   sits between the two blocks rather than tight against this one. */
.sport-team-meta {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  /* Row gap was 0, so Coach/Duration sat straight under Club level and
     Location with only the label's line-height between the rows. 14px of
     row gap separates them without touching the column spacing. */
  gap: 14px 14px;
  margin-top: 14px;
  padding-top: 14px;
  border-top: 1px solid var(--line);
}
/* A third cell has no partner in a two-column grid, so it spans instead
   of sitting half-width with a hole beside it. :last-child:nth-child(odd)
   says "last, and in an odd position" — which in two columns is exactly
   the case of being alone on its row, whatever the count grows to. */
.sport-team-meta div:last-child:nth-child(odd) { grid-column: 1 / -1; }
.sport-team-meta dd { margin: 0; font-size: 13px; }
.sport-team-meta dt { margin-top: 1px; font-size: 11px; color: var(--ink-soft); }

/* One card per tournament. */
.sport-tourney { padding: 16px 18px; }
.sport-tourney:hover { transform: none; box-shadow: var(--shadow); }
.sport-tourney + .sport-tourney { margin-top: 0; }
.sport-tourney-head { display: flex; align-items: center; gap: 10px; }
/* Name over date — the head stacks, so the crest sits beside both. */
.sport-tourney-id { min-width: 0; display: flex; flex-direction: column; }
.sport-tourney-name {
  min-width: 0;
  font-size: 14px;
  font-weight: 600;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.sport-tourney-date { font-size: 11px; color: var(--ink-soft); font-variant-numeric: tabular-nums; }
/* Ruled off from the head, the same treatment the club card gives its
   meta grid — border on the block below, so it can never dangle. */
.sport-tourney-pairs {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 14px;
  margin-top: 14px;
  padding-top: 14px;
  border-top: 1px solid var(--line);
}
/* Three across, because Appearances / Goals / Assists are one set and
   reading them in a row is the point — and now they sit in their own
   inset card, so the eye reads them as a scoreline rather than as three
   more facts of the same kind as Venue and Level.

   The wash is the same one the skill pills wear. Two different greys
   inside one card would read as two different meanings; one grey says
   "these are both insets" and nothing more.

   No gap — the hairlines below do the separating, and a gap would break
   the fill into three floating boxes instead of one strip. */
.sport-tourney-counts {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  margin-top: 14px;
  padding: 8px 4px;   /* trimmed from 12px — the strip sat too tall */
  border-radius: var(--radius-sm);
  background: var(--bg);
}
.sport-tourney-counts > div { text-align: center; }
/* `div + div` skips the first cell, so the strip never opens on a rule. */
.sport-tourney-counts > div + div { border-left: 1px solid var(--line); }
/* Five facts in a two-column grid leaves the last one alone on its row,
   so it spans — the same rule the club card uses, written the same way
   so it keeps working whatever the list grows to. */
.sport-tourney-pairs div:last-child:nth-child(odd) { grid-column: 1 / -1; }
/* Skills are TAGS, so they wear pills — label inline, then the set.
   flex + wrap rather than a grid: the pills are different widths and
   should sit shoulder to shoulder, spilling onto a second line only
   when they run out of room.

   `align-items: baseline` is what keeps the 11px label sitting on the
   same line of type as the 12px pill text; centring them would leave
   the label floating slightly high. */
.sport-tourney-skills {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 6px;
  margin: 14px 0 0;
}
.sport-skills-label {
  margin-right: 2px;
  font-size: 11px;
  color: var(--ink-soft);
}
/* Minimalist: a soft grey wash, no border, no colour. The pill shape
   alone says "this is a tag" — an outline on top of a fill would be
   two ways of saying the same thing. */
.sport-skill-pill {
  padding: 3px 10px;
  border-radius: 999px;
  background: var(--bg);
  color: var(--ink);
  font-size: 12px;
  line-height: 1.5;
  white-space: nowrap;
}
.sport-tourney dd { margin: 0; font-size: 13px; font-variant-numeric: tabular-nums; }
.sport-tourney dt { margin-top: 1px; font-size: 11px; color: var(--ink-soft); }
.sport-tourney-counts dd { font-size: 15px; }


/* The kit, on the club card below the meta grid. The gallery borrows the
   same label — one shelf heading, used twice, so they can't drift. */
.sport-jersey-label { margin-top: 16px; font-size: 12px; font-weight: 600; }
/* …except the gallery's is the card's FIRST element, so it has nothing
   above it to be spaced from. */
.sport-media-label { margin-top: 0; }
.sport-jersey-count {
  display: inline-block;
  margin-left: 5px;
  padding: 1px 6px;
  border-radius: 999px;
  background: var(--bg);
  color: var(--ink-soft);
  font-size: 10px;
  font-variant-numeric: tabular-nums;
}
/* One kit per view, so the arrows page between them. The shared rail
   shows three across, which is right for match photos and wrong for a
   single portrait frame. No max-width any more: the rail runs the full
   width of the card and the FRAME inside decides how wide the kit is,
   from its own height and ratio. */
.sport-jersey-rail { margin: 10px auto 0; padding: 0; }
.sport-jersey-rail .media-rail { grid-auto-columns: 100%; }

/* THE JERSEY FILLS WHATEVER HEIGHT IS LEFT OVER.

   The club card is a flex column, so `flex: 1` on the rail hands it every
   pixel the head, meta grid and label didn't use — which is exactly the
   slack the stretch above created. That's why the kit grows to close the
   gap instead of us guessing a fixed number that would be wrong the moment
   a field is added.

   `align-items: stretch` overrides the shared rail's `center`, so the
   frame inside grows too rather than floating in the middle.

   `width: 100%` is NOT redundant. A flex item with `margin: … auto` on the
   cross axis loses its stretch — that's the spec, auto margins win — so the
   rail fell back to shrink-to-fit and collapsed around the 30px kit icon.
   That's what made it a thin ribbon. The explicit width puts it back. */
.sport-team { display: flex; flex-direction: column; }
.sport-jersey-rail { flex: 1; width: 100%; align-items: stretch; }
.sport-jersey-rail .media-card,
.sport-jersey-rail .media-card a { height: 100%; }
/* The arrows must NOT stretch — a full-height chevron button is a wall. */
.sport-jersey-rail .media-nav { align-self: center; }
/* The frame is sized FROM ITS HEIGHT: it takes the leftover space, then
   the ratio sets the matching width, so the kit stays kit-shaped however
   tall the card grows. `max-width` stops it outgrowing the card on a short
   stack; `min-height` stops it collapsing on an empty one.

   `width: fit-content`, NOT `width: auto`. On a block-level box `auto`
   means "fill the parent" — so the frame took the card's full width and
   the aspect-ratio then drove the HEIGHT from that width, backwards from
   what's wanted here, which is what made the card overshoot the stack.
   Worse, narrowing the ratio made it TALLER under that reading. fit-content
   makes the box shrink-to-fit, so the definite height wins and width
   follows from it. */
.sport-jersey {
  /* 3:4, widened back from 7:10. Height is set by the fill and the width
     follows from the ratio, so this dial moves the WIDTH ONLY — 0.70 →
     0.75 is about 7% wider at exactly the same height. (That was only
     true once `width: fit-content` fixed which dimension leads; under the
     old `width: auto` this same edit would have changed the height.) */
  aspect-ratio: 3 / 4;   /* overrides the shared frame's 4:3 — a kit is portrait */
  height: 100%;
  min-height: 240px;
  width: fit-content;
  max-width: 100%;
  margin: 0 auto;
}
.sport-jersey svg { width: 30px; height: 30px; }

/* THE BOOTS. One per view like the kit, so the arrows page between pairs
   rather than shrinking them to thumbnails.

   Landscape, not portrait: a boot is photographed side-on and a 4:5 frame
   would leave two empty bands above and below it.

   Same fill trick as the jersey, because this card shares row 2 with the
   gallery and whichever is shorter has to grow into the row: the rail
   takes the leftover height, and the frame reads its width off that
   height via fit-content. `width: auto` here would run the ratio
   backwards and push the row taller — see the jersey block above. */
.sport-boots { display: flex; flex-direction: column; }
.sport-boot-rail { margin-top: 12px; padding: 0; flex: 1; width: 100%; align-items: stretch; }
.sport-boot-rail .media-rail { grid-auto-columns: 100%; }
.sport-boot-rail .media-card,
.sport-boot-rail .media-card a { height: 100%; }
.sport-boot-rail .media-nav { align-self: center; }
.sport-boot {
  aspect-ratio: 4 / 3;
  height: 100%;
  /* THE HEIGHT DIAL FOR ROW 2. The boots card is the taller of the two, so
     this floor sets the row, and the gallery beside it stretches to match.
     One number moves both cards — raising it on the gallery instead would
     do nothing, because the gallery isn't what the row is measuring. */
  min-height: 210px;
  width: fit-content;
  max-width: 100%;
  margin: 0 auto;
}
.sport-boot svg { width: 28px; height: 28px; }

/* THE GALLERY. Three-across was wrong here.

   That rail is built for the WIDE cards — Career, Courses, Awards — where
   a third of the card is still a decent photo. This card is one column of
   a pair, so a third of it came out about 75px: a thumbnail floating in a
   210px-tall card.

   So it pages ONE AT A TIME, like the kit and the boots either side of it,
   and the frame fills the row the same way. You still see every photo —
   the arrows walk through them — but each one is worth looking at. */
.sport-gallery { display: flex; flex-direction: column; }
.sport-photo-rail { margin-top: 10px; flex: 1; width: 100%; align-items: stretch; }
.sport-photo-rail .media-rail { grid-auto-columns: 100%; }
.sport-photo-rail .media-card,
.sport-photo-rail .media-card a { height: 100%; }
.sport-photo-rail .media-nav { align-self: center; }
.sport-photo-rail .media-frame {
  aspect-ratio: 4 / 3;   /* landscape — a match photo, not a kit shot */
  height: 100%;
  min-height: 150px;
  width: fit-content;
  max-width: 100%;
  margin: 0 auto;
}

/* The tournament picker. Styled to read as the card's title, not as a
   form control — no chunky border or background, just the name with a
   chevron hinting it opens. appearance: none is what strips the OS
   widget so the rest of the styling actually lands. */
.sport-tourney-select {
  appearance: none;
  -webkit-appearance: none;
  max-width: 100%;
  padding: 0 18px 0 0;
  border: 0;
  background: transparent;
  color: inherit;
  font: inherit;
  font-size: 14px;
  font-weight: 600;
  cursor: pointer;
  /* A chevron drawn as a background image, since a <select> can hold no
     child element of its own. */
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23767b85' stroke-width='2.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m6 9 6 6 6-6'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: right center;
  background-size: 13px 13px;
}
.sport-tourney-select:hover { color: var(--accent); }
.sport-tourney-select:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; border-radius: 3px; }


/* The evidence header, inside a tool card's disclosure. It is what now
   separates the note from the list, so .skill-srcs drops the divider it
   normally draws — a rule between a heading and the list it heads reads as
   a break in the middle of one thing. */
/* The rule sits ABOVE the heading, not between it and its list — so it
   separates one SECTION of the card from the next, which is what a divider
   should do. "Used on" and "Endorsed by" each get one, so a card carrying
   both reads as three parts rather than one long run. */
.evi-head {
  margin-top: 12px;
  padding-top: 10px;
  border-top: 1px solid var(--line);
  font-size: 11px;
  font-weight: 600;
  color: var(--ink-soft);
}
.evi-head + .skill-srcs {
  margin-top: 6px;
  padding-top: 0;
  border-top: none;
}

/* Air above each kind heading after the first, inside a thread's Skills
   card. Scoped to .skill-thread: the Top skills card has its own rule for
   the same job, keyed on the element that precedes it there. */
.skill-thread .skill-cols + .skill-cat { margin-top: 20px; }
/* ...and air BELOW it, before the cards it labels.

   The shared 4px was set for the Top skills card, where the heading is
   followed by list ROWS — text under text, which wants to sit tight.
   Here it is followed by bordered cards, and 4px pinned the heading to
   the first card's edge as though it were part of it.

   Set on the grid rather than as a bigger margin-bottom on the heading,
   so the shared 4px keeps working wherever rows still follow. Adjacent
   margins COLLAPSE, so this doesn't add to that 4px — the larger of the
   two wins and the gap is exactly this number.

   Deliberately smaller than the 20px above: a heading should sit nearer
   what it labels than what it follows, or it reads as belonging to the
   section that came before. */
.skill-thread .skill-cat + .skill-cols { margin-top: 10px; }

/* ---- endorsements ----
   Sits under the evidence: the sources say where a skill was used, this
   says who will stand behind it. Proof first, then the person.

   BUILT TO MATCH .skill-src ABOVE IT, on request — same row shape, same
   26px frame, same 13px/11px type. The two lists sit one under the other
   inside one card, so anything else read as two designs sharing a box.
   Only the frame's shape differs: round for a face, rounded-square for a
   logo. */
.endorse-list { list-style: none; display: flex; flex-direction: column; gap: 8px; margin-top: 6px; }
.endorse { display: flex; align-items: center; gap: 8px; min-width: 0; }
.endorse-photo {
  position: relative;
  flex: none;
  width: 30px;
  height: 30px;
  /* The one deliberate difference from .skill-src-logo. */
  border-radius: 50%;
  border: 1px solid var(--line);
  background: var(--bg);
  display: flex;
  align-items: center;
  justify-content: center;
  color: var(--ink-soft);
  overflow: hidden;
}
.endorse-photo svg { width: 15px; height: 15px; }
/* cover, not contain: a face should fill its circle. A logo must never be
   cropped, which is why .skill-src-logo contains instead — the same reason
   pulling in opposite directions. */
.endorse-photo img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; }
/* min-width:0 on BOTH the flex child and its children, or the ellipsis
   never fires — flex items default to min-width:auto. */
.endorse-text { display: flex; flex-direction: column; min-width: 0; }
.endorse-who,
.endorse-role { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.endorse-who { font-size: 13px; color: var(--ink); }
.endorse-role { margin-top: 1px; font-size: 11px; color: var(--ink-soft); }
/* Stacks its inputs instead of sitting them in a row: a source is two
   short dropdowns, a person is a photo, a name, a role and a workplace. */
.car-media-item.is-stack { display: flex; flex-direction: column; gap: 6px; align-items: stretch; }
/* The editor's portrait preview is round too, so it shows what the card
   will actually draw rather than a square that later gets cropped. */
.endorse-pick { border-radius: 50%; }

/* ---- Education's See more, ON the summary line ----
   On request: the control sits at the END of the summary rather than on a
   line of its own. That kills the base rule's `margin-top: 10px`, which
   exists to separate a block-level toggle from the text above it — inline,
   it would push the whole line down instead.

   The summary is 14px/600; the button deliberately is NOT. Matching the
   line's weight would make "See more" read as part of the sentence, which
   is exactly the wrong thing for a control. Lighter and softer, so it
   trails the text as an affordance rather than continuing it. */
.edu-feature-summary .timeline-toggle.edu-more {
  display: inline;
  margin-top: 0;
  margin-left: 8px;
  font-size: 13px;
  font-weight: 400;
  color: var(--ink-soft);
  /* The line can wrap; the button should travel with the last word rather
     than being stranded alone on a line of its own. */
  white-space: nowrap;
}
.edu-feature-summary .timeline-toggle.edu-more:hover { color: var(--ink); }

/* ============================================================
   HOME TOP ROW — the hero, and a welcome card filling the rest
   ============================================================ */
/* `align-items: stretch` is the whole trick: the two cards match heights
   because the shorter one grows to the taller, so nothing here states a
   height. Pin one and the pair breaks apart the moment the greeting wraps
   to a second line on a narrower window. */
.home-top {
  display: flex;
  align-items: stretch;
  gap: 20px;
  margin-bottom: 8px;
  flex-wrap: wrap;          /* below ~1100px the welcome card drops under */
}
/* `0 1 680px` rather than a plain max-width: it holds 680 when there is
   room, and is allowed to SHRINK when there isn't — but never grows to
   eat the space the welcome card is there to fill. */
.home-top .home-hero {
  flex: 0 1 680px;
  max-width: 680px;
  margin-bottom: 0;         /* the row owns the spacing now */
}
/* Takes whatever is left. min-width stops a long quote forcing the row
   wider than the page; 300px is the point below which it wraps instead. */
.home-welcome {
  flex: 1 1 300px;
  min-width: 0;
  display: flex;
  flex-direction: column;
  justify-content: center;
  gap: 10px;
  padding: 38px 32px;       /* matches the hero, so the two read as a pair */
}
.home-greet { font-size: 15px; color: var(--ink-soft); }
/* A blockquote, because that is what it is — and browsers indent one by
   default, which is why the margin is reset. */
.home-quote {
  margin: 0;
  font-size: 18px;
  line-height: 1.45;
  color: var(--ink-strong);
}
.home-quote-by { font-size: 13px; color: var(--ink-soft); }
