/* ============================================================================
   ResumeD — Design system
   ============================================================================

   Single source of truth for the UI. Vanilla CSS, no build step, no framework.

   How it is organised
   -------------------
   1. Primitives     raw palette + scales. Never used directly by a component.
   2. Semantic tokens meaning-based aliases (--c-primary, --c-text...). Themes
                      only ever redefine this layer.
   3. Themes          light (default), dark, daltonism. Mutually exclusive,
                      selected with data-theme on <html>.
   4. Base            reset and element defaults.
   5. Components      .btn, .field, .alert... Every variant is expressed by
                      remapping local custom properties, never by duplicating
                      the property block.
   6. Utilities       small single-purpose helpers.

   Contrast: every text/background pair below is checked against WCAG. Light and
   dark themes meet AA (>= 4.5:1 on body text), the daltonism theme meets AAA
   (>= 7:1) and never encodes meaning with hue alone — semantic components
   always carry an icon as well.
   ========================================================================= */


/* ============================================================================
   1. PRIMITIVES
   ========================================================================= */

:root {
  /* Brand green. --green-500 is the requested #2fad18: it is kept as-is and
     used where it passes AA (as a colour on dark surfaces). Filled surfaces in
     the light theme use the darker steps, since #2fad18 under white text only
     reaches 2.95:1. */
  --green-50:  #f3fdf0;
  --green-100: #e8fbe4;
  --green-200: #d8f9d2;
  --green-300: #b9f4ae;
  --green-400: #3de01f;
  --green-500: #2fad18;
  --green-600: #227d11;
  --green-700: #1b630e;
  --green-800: #13480a;
  --green-900: #0c2d06;

  /* Warm neutrals, LinkedIn-like (its canvas is a warm off-white, not blue-grey) */
  --neutral-0:   #ffffff;
  --neutral-50:  #faf9f7;
  --neutral-100: #f4f2ee;
  --neutral-200: #e8e6e1;
  --neutral-300: #d6d3cd;
  --neutral-400: #b0aca4;
  --neutral-500: #8b877e;
  --neutral-600: #5e5b55;
  --neutral-700: #45423d;
  --neutral-800: #2b2926;
  --neutral-900: #1c1b19;
  --neutral-950: #101318;

  --red-100:  #fdecea;
  --red-300:  #f28b82;
  --red-600:  #b3261e;
  --red-700:  #a3211a;
  --red-800:  #8c1c16;
  --red-900:  #9a0000;

  --amber-100: #fdf3e0;
  --amber-300: #f5c26b;
  --amber-600: #7a4f00;
  --amber-700: #6b4400;
  --amber-800: #5c3b00;
  --amber-900: #4d3300;

  --blue-100: #e7f1fb;
  --blue-300: #7cb9f2;
  --blue-600: #0a66c2;
  --blue-700: #08529b;
  --blue-800: #0a4a9e;
  --blue-900: #003a8c;

  /* Magenta exists only for the daltonism theme. A pure red and a green are the
     same colour to a protanope (dE 6.5, i.e. indistinguishable); shifting danger
     towards magenta keeps a blue component that red-green blindness preserves,
     which raises the separation to dE 40+. */
  --magenta-100: #ffe6f2;
  --magenta-700: #a80060;
  --magenta-800: #8a004e;
  --magenta-900: #6b003c;

  /* ---- Non-colour primitives (shared by every theme) ---- */

  --font-sans: system-ui, -apple-system, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
  --font-mono: ui-monospace, "Cascadia Code", "SF Mono", Consolas, monospace;

  --text-xs:   0.75rem;   /* 12px — legal, captions        */
  --text-sm:   0.8125rem; /* 13px — metadata, table body   */
  --text-base: 0.875rem;  /* 14px — LinkedIn body size     */
  --text-md:   1rem;      /* 16px — inputs (avoids iOS zoom) */
  --text-lg:   1.125rem;  /* 18px — card titles            */
  --text-xl:   1.375rem;  /* 22px — page titles            */
  --text-2xl:  1.75rem;   /* 28px — hero                   */

  --weight-normal: 400;
  --weight-medium: 500;
  --weight-bold:   600;

  --leading-tight: 1.25;
  --leading-base:  1.5;

  /* 4px base scale */
  --space-1: 4px;
  --space-2: 8px;
  --space-3: 12px;
  --space-4: 16px;
  --space-5: 24px;
  --space-6: 32px;
  --space-7: 48px;
  --space-8: 64px;

  --radius-sm:   4px;
  --radius-md:   8px;
  --radius-lg:   12px;
  --radius-pill: 999px;

  --border-width: 1px;

  --control-height-sm: 28px;
  --control-height-md: 40px;
  --control-height-lg: 48px;

  --focus-width:  2px;
  --focus-offset: 2px;

  --transition: 120ms ease-in-out;

  --layout-max: 1128px;      /* LinkedIn's content width */
  --layout-narrow: 420px;
}


/* ============================================================================
   2 + 3. SEMANTIC TOKENS AND THEMES

   Three mutually exclusive themes. data-theme is written on <html> by theme.js
   before first paint; the light values below are the fallback if JS is off.
   ========================================================================= */

:root,
[data-theme="light"] {
  color-scheme: light;

  --c-bg:            var(--neutral-100);
  --c-surface:       var(--neutral-0);
  --c-surface-sunken: var(--neutral-50);
  --c-surface-hover: var(--neutral-100);
  --c-overlay:       rgb(0 0 0 / 0.5);

  --c-border:        var(--neutral-300);
  /* neutral-500, not 400: WCAG requires 3:1 for the outline of an interactive
     component, and neutral-400 only reached 2.26:1 on white. */
  --c-border-strong: var(--neutral-500);

  --c-text:          var(--neutral-900);
  --c-text-muted:    var(--neutral-600);
  --c-text-inverse:  var(--neutral-0);

  /* Semantic families. Each exposes the same five slots so components can be
     recoloured by swapping one variable instead of restating a rule. */
  --c-primary:          var(--green-600);  /* filled surface              */
  --c-primary-hover:    var(--green-700);
  --c-primary-active:   var(--green-800);
  --c-primary-on:       var(--neutral-0);  /* text/icon on filled surface */
  --c-primary-text:     var(--green-700);  /* same family, as text/border */
  --c-primary-subtle:   var(--green-100);  /* tinted background           */

  --c-danger:           var(--red-600);
  --c-danger-hover:     var(--red-700);
  --c-danger-active:    var(--red-800);
  --c-danger-on:        var(--neutral-0);
  --c-danger-text:      var(--red-700);
  --c-danger-subtle:    var(--red-100);

  --c-success:          var(--green-600);
  --c-success-hover:    var(--green-700);
  --c-success-active:   var(--green-800);
  --c-success-on:       var(--neutral-0);
  --c-success-text:     var(--green-700);
  --c-success-subtle:   var(--green-100);

  --c-warning:          var(--amber-700);
  --c-warning-hover:    var(--amber-900);
  --c-warning-active:   var(--amber-900);
  --c-warning-on:       var(--neutral-0);
  --c-warning-text:     var(--amber-700);
  --c-warning-subtle:   var(--amber-100);

  --c-info:             var(--blue-600);
  --c-info-hover:       var(--blue-700);
  --c-info-active:      var(--blue-900);
  --c-info-on:          var(--neutral-0);
  --c-info-text:        var(--blue-700);
  --c-info-subtle:      var(--blue-100);

  --c-focus: var(--green-700);

  --shadow-sm: 0 1px 2px rgb(28 27 25 / 0.10);
  --shadow-md: 0 2px 4px rgb(28 27 25 / 0.10), 0 4px 12px rgb(28 27 25 / 0.06);
  --shadow-lg: 0 8px 28px rgb(28 27 25 / 0.18);
}

[data-theme="dark"] {
  color-scheme: dark;

  --c-bg:            #1b1f23;
  --c-surface:       #22272b;
  --c-surface-sunken: #1b1f23;
  --c-surface-hover: #2c3237;
  --c-overlay:       rgb(0 0 0 / 0.65);

  --c-border:        #3a4046;
  --c-border-strong: #545c63;

  --c-text:          #f2f1ef;
  --c-text-muted:    #a8a49c;
  --c-text-inverse:  var(--neutral-950);

  /* On dark surfaces the light steps carry the colour and dark text sits on the
     fill — same inversion LinkedIn applies to its blue in dark mode. */
  --c-primary:          var(--green-500);
  --c-primary-hover:    var(--green-400);
  --c-primary-active:   var(--green-300);
  --c-primary-on:       var(--neutral-950);
  --c-primary-text:     var(--green-500);
  --c-primary-subtle:   rgb(47 173 24 / 0.16);

  --c-danger:           var(--red-300);
  --c-danger-hover:     #f7aaa3;
  --c-danger-active:    #fbc5c0;
  --c-danger-on:        var(--neutral-950);
  --c-danger-text:      var(--red-300);
  --c-danger-subtle:    rgb(242 139 130 / 0.16);

  --c-success:          var(--green-500);
  --c-success-hover:    var(--green-400);
  --c-success-active:   var(--green-300);
  --c-success-on:       var(--neutral-950);
  --c-success-text:     var(--green-500);
  --c-success-subtle:   rgb(47 173 24 / 0.16);

  --c-warning:          var(--amber-300);
  --c-warning-hover:    #f8d18f;
  --c-warning-active:   #fadfb3;
  --c-warning-on:       var(--neutral-950);
  --c-warning-text:     var(--amber-300);
  --c-warning-subtle:   rgb(245 194 107 / 0.16);

  --c-info:             var(--blue-300);
  --c-info-hover:       #9dcbf6;
  --c-info-active:      #bedcf9;
  --c-info-on:          var(--neutral-950);
  --c-info-text:        var(--blue-300);
  --c-info-subtle:      rgb(124 185 242 / 0.16);

  --c-focus: var(--green-400);

  --shadow-sm: 0 1px 2px rgb(0 0 0 / 0.5);
  --shadow-md: 0 2px 4px rgb(0 0 0 / 0.5), 0 4px 12px rgb(0 0 0 / 0.35);
  --shadow-lg: 0 8px 28px rgb(0 0 0 / 0.6);
}

/* Daltonism / high-contrast theme.
   Two rules drive it.

   1. Every pair clears AAA (7:1).
   2. The semantic families stay apart under protanopia, deuteranopia AND
      tritanopia — not just for normal vision. The palette was picked by
      simulating each type and maximising the worst pairwise CIE dE76:

                          before      after
        worst red-green      6.0       27.8   (dE < 10 reads as "same colour")
        worst tritanopia    16.0       26.2

      Concretely: danger moved from a pure red to a magenta (red-green blindness
      preserves its blue component), success went darker so it separates from
      warning on the lightness axis, and warning moved lighter for the same
      reason.

   Pale tinted backgrounds cannot be told apart under any simulation — they carry
   too little chroma — which is why every semantic component in this system also
   carries an icon and a 6px rule in the strong colour. Borders and focus rings
   are thickened so shape carries information too. */
[data-theme="daltonism"] {
  color-scheme: light;

  --c-bg:            var(--neutral-0);
  --c-surface:       var(--neutral-0);
  --c-surface-sunken: #f2f2f2;
  --c-surface-hover: #e6e6e6;
  --c-overlay:       rgb(0 0 0 / 0.7);

  --c-border:        #000000;
  --c-border-strong: #000000;

  --c-text:          #000000;
  --c-text-muted:    #333333;
  --c-text-inverse:  #ffffff;

  /* green-900 rather than 800: pushed down the lightness axis so it stays
     distinct from warning, which red-green blindness maps to the same hue. */
  --c-primary:          var(--green-900);
  --c-primary-hover:    #061a03;
  --c-primary-active:   #000000;
  --c-primary-on:       #ffffff;
  --c-primary-text:     var(--green-900);
  --c-primary-subtle:   var(--green-100);

  --c-danger:           var(--magenta-700);
  --c-danger-hover:     var(--magenta-800);
  --c-danger-active:    var(--magenta-900);
  --c-danger-on:        #ffffff;
  /* Darker than the fill: the fill is only 7.41:1 on white, so text sitting on
     the tinted background needs its own step to stay above 7:1. */
  --c-danger-text:      var(--magenta-800);
  --c-danger-subtle:    var(--magenta-100);

  --c-success:          var(--green-900);
  --c-success-hover:    #061a03;
  --c-success-active:   #000000;
  --c-success-on:       #ffffff;
  --c-success-text:     var(--green-900);
  --c-success-subtle:   var(--green-100);

  --c-warning:          var(--amber-600);
  --c-warning-hover:    var(--amber-800);
  --c-warning-active:   var(--amber-900);
  --c-warning-on:       #ffffff;
  --c-warning-text:     var(--amber-800);
  --c-warning-subtle:   #fff2d9;

  --c-info:             var(--blue-800);
  --c-info-hover:       var(--blue-900);
  --c-info-active:      #002a66;
  --c-info-on:          #ffffff;
  --c-info-text:        var(--blue-900);
  --c-info-subtle:      #e0ecff;

  --c-focus: #000000;

  --border-width: 2px;
  --focus-width:  3px;

  --shadow-sm: none;
  --shadow-md: none;
  --shadow-lg: 0 0 0 2px #000000;
}


/* ============================================================================
   4. BASE
   ========================================================================= */

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

html { -webkit-text-size-adjust: 100%; }

body {
  margin: 0;
  background: var(--c-bg);
  color: var(--c-text);
  font-family: var(--font-sans);
  font-size: var(--text-base);
  line-height: var(--leading-base);
}

h1, h2, h3, h4 {
  margin: 0 0 var(--space-3);
  line-height: var(--leading-tight);
  font-weight: var(--weight-bold);
}

h1 { font-size: var(--text-xl); }
h2 { font-size: var(--text-lg); }
h3 { font-size: var(--text-md); }
h4 { font-size: var(--text-base); }

p { margin: 0 0 var(--space-3); }

a {
  color: var(--c-primary-text);
  text-decoration: none;
  font-weight: var(--weight-bold);
}

a:hover { text-decoration: underline; }

small { font-size: var(--text-xs); }

code, pre, kbd { font-family: var(--font-mono); }

hr {
  border: 0;
  border-top: var(--border-width) solid var(--c-border);
  margin: var(--space-5) 0;
}

/* Single focus treatment for the whole system. Keyboard only, so pointer users
   never see a ring they did not ask for. */
:focus-visible {
  outline: var(--focus-width) solid var(--c-focus);
  outline-offset: var(--focus-offset);
  border-radius: var(--radius-sm);
}

[hidden] { display: none !important; }

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


/* ============================================================================
   5. COMPONENTS
   ========================================================================= */

/* ---------- Layout ---------- */

.page {
  max-width: var(--layout-max);
  margin: 0 auto;
  padding: var(--space-5) var(--space-4) var(--space-8);
}

.page-narrow {
  max-width: var(--layout-narrow);
  margin: var(--space-7) auto;
  padding: 0 var(--space-4);
}

.stack     { display: flex; flex-direction: column; gap: var(--space-3); }
.stack-lg  { display: flex; flex-direction: column; gap: var(--space-5); }
.row       { display: flex; align-items: center; gap: var(--space-3); flex-wrap: wrap; }
.row-tight { display: flex; align-items: center; gap: var(--space-2); flex-wrap: wrap; }
.spacer    { flex: 1; }


/* ---------- Icon ----------
   <svg class="icon" aria-hidden="true"><use href="/img/icons.svg#x"></use></svg>
   Sizes follow the text (em), colour follows currentColor. */

.icon {
  width: 1.25em;
  height: 1.25em;
  flex: none;
  vertical-align: -0.2em;
  stroke: currentColor;
  fill: none;
  pointer-events: none;
}

.icon-sm { width: 1em;    height: 1em; }
.icon-lg { width: 1.5em;  height: 1.5em; }
.icon-xl { width: 2.5em;  height: 2.5em; }

/* Multi-colour marks (the Google logo). Same box as .icon, but the symbol
   keeps the fills it declares instead of following currentColor. */
.icon-color {
  width: 1.25em;
  height: 1.25em;
  flex: none;
  vertical-align: -0.2em;
  pointer-events: none;
}

/* Single-colour solid marks (platform logos). ADDS to .icon, whose box it
   keeps — otherwise it would override .icon-sm.

   Lucide icons are PATHS: they follow the text through `stroke`. A solid mark
   has no path but a surface — it follows the text through `fill`, and its
   stroke must be explicitly cut, or it thickens by a line that closes its
   counters. */
.icon-solid {
  fill: currentColor;
  stroke: none;
}


/* ---------- Buttons ----------
   One rule, four local variables. Every variant only remaps the variables,
   which is why there is no duplicated property block below. */

.btn {
  --btn-bg:           var(--c-primary);
  --btn-fg:           var(--c-primary-on);
  --btn-border:       transparent;
  --btn-bg-hover:     var(--c-primary-hover);
  --btn-bg-active:    var(--c-primary-active);
  --btn-border-hover: transparent;

  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-2);
  min-height: var(--control-height-md);
  padding: 0 var(--space-4);
  border: var(--border-width) solid var(--btn-border);
  border-radius: var(--radius-pill);
  background: var(--btn-bg);
  color: var(--btn-fg);
  font: inherit;
  font-weight: var(--weight-bold);
  line-height: 1;
  text-align: center;
  text-decoration: none;
  cursor: pointer;
  transition: background var(--transition), border-color var(--transition), box-shadow var(--transition);
}

.btn:hover {
  background: var(--btn-bg-hover);
  border-color: var(--btn-border-hover);
  text-decoration: none;
  /* Thickens the outline on hover without shifting layout (LinkedIn's trick) */
  box-shadow: inset 0 0 0 1px var(--btn-border-hover);
}

.btn:active { background: var(--btn-bg-active); }

.btn:disabled,
.btn[aria-disabled="true"] {
  opacity: 0.45;
  cursor: not-allowed;
  box-shadow: none;
  background: var(--btn-bg);
  border-color: var(--btn-border);
}

/* Outlined: the family colour on the border and the text, transparent fill */
.btn-secondary {
  --btn-bg:           transparent;
  --btn-fg:           var(--c-primary-text);
  --btn-border:       var(--c-primary-text);
  --btn-bg-hover:     var(--c-primary-subtle);
  --btn-bg-active:    var(--c-primary-subtle);
  --btn-border-hover: var(--c-primary-text);
}

/* Quiet: no chrome until hovered */
.btn-tertiary {
  --btn-bg:           transparent;
  --btn-fg:           var(--c-text-muted);
  --btn-border:       transparent;
  --btn-bg-hover:     var(--c-surface-hover);
  --btn-bg-active:    var(--c-border);
  --btn-border-hover: transparent;
}

.btn-danger {
  --btn-bg:           var(--c-danger);
  --btn-fg:           var(--c-danger-on);
  --btn-bg-hover:     var(--c-danger-hover);
  --btn-bg-active:    var(--c-danger-active);
}

.btn-danger-outline {
  --btn-bg:           transparent;
  --btn-fg:           var(--c-danger-text);
  --btn-border:       var(--c-danger-text);
  --btn-bg-hover:     var(--c-danger-subtle);
  --btn-bg-active:    var(--c-danger-subtle);
  --btn-border-hover: var(--c-danger-text);
}

/* Neutral outlined button, for "sign in with Google" style rows */
.btn-neutral {
  --btn-bg:           var(--c-surface);
  --btn-fg:           var(--c-text);
  --btn-border:       var(--c-border-strong);
  --btn-bg-hover:     var(--c-surface-hover);
  --btn-bg-active:    var(--c-border);
  --btn-border-hover: var(--c-border-strong);
}

.btn-sm { min-height: var(--control-height-sm); padding: 0 var(--space-3); font-size: var(--text-sm); }
.btn-lg { min-height: var(--control-height-lg); padding: 0 var(--space-5); font-size: var(--text-md); }

.btn-block { display: flex; width: 100%; }

/* Icon-only. Requires aria-label — the cross-shaped delete button is the
   canonical case: .btn .btn-icon .btn-danger-outline + #x */
.btn-icon {
  width: var(--control-height-md);
  min-height: var(--control-height-md);
  padding: 0;
}

.btn-icon.btn-sm { width: var(--control-height-sm); min-height: var(--control-height-sm); }
.btn-icon.btn-lg { width: var(--control-height-lg); min-height: var(--control-height-lg); }

/* Loading state: .btn[data-loading] swaps the label for a spinner.
   The button keeps its width, so the row does not reflow. */
.btn > .spinner { display: none; }

.btn[data-loading] {
  position: relative;
  pointer-events: none;
  opacity: 0.7;
}

.btn[data-loading] > :not(.spinner) { visibility: hidden; }
.btn[data-loading] > .spinner { display: block; position: absolute; }


/* ---------- Form fields ---------- */

.field { margin-bottom: var(--space-4); }

.label,
.field > label {
  display: block;
  margin-bottom: var(--space-1);
  font-size: var(--text-sm);
  font-weight: var(--weight-bold);
  color: var(--c-text);
}

.label-optional {
  margin-left: var(--space-1);
  font-weight: var(--weight-normal);
  color: var(--c-text-muted);
}

.input,
.select,
.textarea {
  width: 100%;
  min-height: var(--control-height-md);
  padding: var(--space-2) var(--space-3);
  border: var(--border-width) solid var(--c-border-strong);
  border-radius: var(--radius-sm);
  background: var(--c-surface);
  color: var(--c-text);
  font: inherit;
  font-size: var(--text-md);
  transition: border-color var(--transition), box-shadow var(--transition);
}

.textarea { min-height: 96px; resize: vertical; }

.input::placeholder,
.textarea::placeholder { color: var(--c-text-muted); }

.input:hover,
.select:hover,
.textarea:hover { border-color: var(--c-text-muted); }

/* Doubling the border on focus rather than only drawing an outline keeps the
   field readable in the daltonism theme, where hue must not be the only cue. */
.input:focus,
.select:focus,
.textarea:focus {
  outline: var(--focus-width) solid var(--c-focus);
  outline-offset: 0;
  border-color: var(--c-focus);
}

.input:disabled,
.select:disabled,
.textarea:disabled {
  background: var(--c-surface-sunken);
  color: var(--c-text-muted);
  cursor: not-allowed;
}

.input[aria-invalid="true"],
.textarea[aria-invalid="true"],
.select[aria-invalid="true"] {
  border-color: var(--c-danger-text);
  box-shadow: inset 0 0 0 1px var(--c-danger-text);
}

/* The native arrow is replaced by the sprite chevron, so the select matches
   every other icon in the system instead of using a CSS-drawn triangle. */
.select { appearance: none; padding-right: var(--space-6); }

/* Control decorated with an icon (search, email, select chevron, password eye).
   The icon sits on top of the control and never intercepts clicks. */
.input-group { position: relative; display: flex; align-items: center; }

.input-group > .icon {
  position: absolute;
  left: var(--space-3);
  color: var(--c-text-muted);
}

.input-group > .icon-trailing { left: auto; right: var(--space-3); }

.input-group > .icon:not(.icon-trailing) ~ .input,
.input-group > .icon:not(.icon-trailing) ~ .textarea { padding-left: var(--space-6); }

.input-group > .btn-icon { position: absolute; right: var(--space-1); }
.input-group:has(> .btn-icon) .input { padding-right: calc(var(--control-height-md) + var(--space-1)); }

.hint {
  margin: var(--space-1) 0 0;
  font-size: var(--text-xs);
  color: var(--c-text-muted);
}

/* Error text always ships with its icon: colour alone must not carry meaning */
.field-error {
  display: flex;
  align-items: center;
  gap: var(--space-1);
  margin: var(--space-1) 0 0;
  min-height: 1.1em;
  font-size: var(--text-xs);
  font-weight: var(--weight-bold);
  color: var(--c-danger-text);
}

/* Pinned in px: at 12px text a 1em icon becomes unreadable */
.field-error .icon { width: 15px; height: 15px; }

.field-error:empty { display: none; }

/* ============================================================
   Image picker (Fields.images)

   One thumbnail per image, an add button after them, and the whole area
   accepts a dropped file. Thumbnails have a FIXED size and a chequerboard
   background: a transparent icon on a plain background is indistinguishable
   from a white one, and that is exactly the kind of file uploaded here.
   ============================================================ */

.image-picker {
  display: flex;
  flex-wrap: wrap;
  /* Enough air that the remove cross and the rank number, which overflow the
     thumbnails, do not land on the neighbour. */
  gap: var(--space-4);
  align-items: center;
  margin-top: var(--space-1);
  padding: var(--space-3);
  border: var(--border-width) dashed var(--c-border);
  border-radius: var(--radius-sm);
  transition: border-color var(--transition), background var(--transition);
}

.image-picker.is-dropping {
  border-color: var(--c-primary);
  background: var(--c-primary-subtle);
}

.image-thumb {
  position: relative;
  width: 64px;
  height: 64px;
  border: var(--border-width) solid var(--c-border);
  border-radius: var(--radius-sm);
  /* The chequerboard: two crossed gradients, no image to load. */
  background-color: #fff;
  background-image:
    linear-gradient(45deg, #e8e8e8 25%, transparent 25%, transparent 75%, #e8e8e8 75%),
    linear-gradient(45deg, #e8e8e8 25%, transparent 25%, transparent 75%, #e8e8e8 75%);
  background-size: 12px 12px;
  background-position: 0 0, 6px 6px;
}

.image-thumb img {
  width: 100%;
  height: 100%;
  /* `contain`: a wide logo and a square portrait must both be fully visible.
     Cropping would make the thumbnail lie about what will print. */
  object-fit: contain;
  border-radius: var(--radius-sm);
}

/* The rank, when order matters: a skill's icons print in this order, and an
   order that can be changed has to be readable first. */
.image-rank {
  position: absolute;
  bottom: -6px;
  left: -6px;
  min-width: 18px;
  height: 18px;
  padding: 0 4px;
  border-radius: var(--radius-pill);
  background: var(--c-surface);
  border: var(--border-width) solid var(--c-border);
  color: var(--c-text-muted);
  font-size: 11px;
  line-height: 16px;
  text-align: center;
}

/* A reorderable thumbnail: the cursor says so before the first attempt. */
.image-picker[role="list"] .image-thumb {
  cursor: grab;
}

.image-thumb.is-dragging { opacity: 0.4; }

/* A row: the line goes left or right, not top or bottom. An outer `box-shadow`
   rather than an inset one — a thumbnail is small, and a line taken out of its
   surface would eat into the image. */
.image-thumb.is-drop-before { box-shadow: -3px 0 0 0 var(--c-primary); }
.image-thumb.is-drop-after { box-shadow: 3px 0 0 0 var(--c-primary); }

.image-thumb:focus-visible {
  outline: var(--focus-width) solid var(--c-focus);
  outline-offset: var(--focus-offset);
}

.image-remove {
  position: absolute;
  top: -8px;
  right: -8px;
  display: flex;
  align-items: center;
  justify-content: center;
  width: 22px;
  height: 22px;
  padding: 0;
  border: var(--border-width) solid var(--c-border);
  border-radius: var(--radius-pill);
  background: var(--c-surface);
  color: var(--c-text-muted);
  cursor: pointer;
  transition: background var(--transition), color var(--transition);
}

.image-remove:hover {
  background: var(--c-danger-subtle);
  color: var(--c-danger-text);
}

.image-add {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 64px;
  height: 64px;
  padding: 0;
  border: var(--border-width) dashed var(--c-border);
  border-radius: var(--radius-sm);
  background: var(--c-surface);
  color: var(--c-text-muted);
  cursor: pointer;
  transition: background var(--transition), color var(--transition), border-color var(--transition);
}

.image-add:hover {
  border-color: var(--c-primary);
  background: var(--c-surface-hover);
  color: var(--c-primary-text);
}

.image-remove:focus-visible,
.image-add:focus-visible {
  outline: var(--focus-width) solid var(--c-focus);
  outline-offset: var(--focus-offset);
}


/* ---------- Checkbox, radio, switch ---------- */

.choice {
  display: flex;
  align-items: flex-start;
  gap: var(--space-2);
  margin-bottom: var(--space-2);
  font-size: var(--text-base);
  cursor: pointer;
}

.choice input[type="checkbox"],
.choice input[type="radio"] {
  flex: none;
  width: 18px;
  height: 18px;
  margin: 2px 0 0;
  accent-color: var(--c-primary);
  cursor: pointer;
}

.choice:has(input:disabled) { color: var(--c-text-muted); cursor: not-allowed; }

.switch {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  cursor: pointer;
}

.switch input { position: absolute; opacity: 0; width: 0; height: 0; }

.switch-track {
  position: relative;
  width: 40px;
  height: 22px;
  flex: none;
  border: var(--border-width) solid var(--c-border-strong);
  border-radius: var(--radius-pill);
  background: var(--c-surface-sunken);
  transition: background var(--transition), border-color var(--transition);
}

.switch-track::after {
  content: "";
  position: absolute;
  top: 2px;
  left: 2px;
  width: 14px;
  height: 14px;
  border-radius: 50%;
  background: var(--c-text-muted);
  transition: transform var(--transition), background var(--transition);
}

.switch input:checked + .switch-track {
  background: var(--c-primary);
  border-color: var(--c-primary);
}

.switch input:checked + .switch-track::after {
  transform: translateX(18px);
  background: var(--c-primary-on);
}

.switch input:focus-visible + .switch-track {
  outline: var(--focus-width) solid var(--c-focus);
  outline-offset: var(--focus-offset);
}


/* ---------- Card ---------- */

.card {
  background: var(--c-surface);
  border: var(--border-width) solid var(--c-border);
  border-radius: var(--radius-md);
  box-shadow: var(--shadow-sm);
  padding: var(--space-5);
  margin-bottom: var(--space-5);
}

.card-narrow { max-width: var(--layout-narrow); margin: var(--space-7) auto; }

.card-header {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  margin: calc(var(--space-5) * -1) calc(var(--space-5) * -1) var(--space-4);
  padding: var(--space-4) var(--space-5);
  border-bottom: var(--border-width) solid var(--c-border);
}

.card-header > h2, .card-header > h3 { margin: 0; }


/* ---------- Badge ----------
   Same variable-remapping pattern as .btn. */

.badge {
  --badge-bg: var(--c-surface-hover);
  --badge-fg: var(--c-text-muted);

  display: inline-flex;
  align-items: center;
  gap: var(--space-1);
  padding: 2px var(--space-2);
  border: var(--border-width) solid transparent;
  border-radius: var(--radius-pill);
  background: var(--badge-bg);
  color: var(--badge-fg);
  font-size: var(--text-xs);
  font-weight: var(--weight-bold);
  text-transform: uppercase;
  letter-spacing: 0.03em;
  white-space: nowrap;
}

.badge-primary { --badge-bg: var(--c-primary-subtle); --badge-fg: var(--c-primary-text); }
.badge-success { --badge-bg: var(--c-success-subtle); --badge-fg: var(--c-success-text); }
.badge-warning { --badge-bg: var(--c-warning-subtle); --badge-fg: var(--c-warning-text); }
.badge-danger  { --badge-bg: var(--c-danger-subtle);  --badge-fg: var(--c-danger-text); }
.badge-info    { --badge-bg: var(--c-info-subtle);    --badge-fg: var(--c-info-text); }

/* In the daltonism theme the tinted background is too weak on its own, so the
   badge gains a border that reuses its own text colour. */
[data-theme="daltonism"] .badge { border-color: currentColor; }


/* ---------- Chip (removable tag) ---------- */

.chip {
  display: inline-flex;
  align-items: center;
  gap: var(--space-1);
  padding: var(--space-1) var(--space-1) var(--space-1) var(--space-3);
  border: var(--border-width) solid var(--c-border-strong);
  border-radius: var(--radius-pill);
  background: var(--c-surface);
  font-size: var(--text-sm);
}

.chip .btn-icon {
  width: 20px;
  min-height: 20px;
  border-radius: 50%;
}


/* ---------- Alert ----------
   Always rendered with an icon. In the daltonism theme hue is not a reliable
   channel, so the left rule and the icon are what carry the meaning. */

.alert {
  --alert-bg: var(--c-info-subtle);
  --alert-fg: var(--c-info-text);

  display: flex;
  align-items: flex-start;
  gap: var(--space-2);
  padding: var(--space-3) var(--space-4);
  margin: var(--space-3) 0;
  border: var(--border-width) solid transparent;
  border-left: 4px solid var(--alert-fg);
  border-radius: var(--radius-sm);
  background: var(--alert-bg);
  color: var(--alert-fg);
  font-size: var(--text-base);
}

.alert .icon { margin-top: 2px; }
.alert-body { flex: 1; }
.alert-body > :last-child { margin-bottom: 0; }
.alert-title { font-weight: var(--weight-bold); }

.alert.success { --alert-bg: var(--c-success-subtle); --alert-fg: var(--c-success-text); }
.alert.warning { --alert-bg: var(--c-warning-subtle); --alert-fg: var(--c-warning-text); }
.alert.error   { --alert-bg: var(--c-danger-subtle);  --alert-fg: var(--c-danger-text); }
.alert.info    { --alert-bg: var(--c-info-subtle);    --alert-fg: var(--c-info-text); }

[data-theme="daltonism"] .alert { border-color: currentColor; border-left-width: 6px; }


/* ---------- Toast ---------- */

/* Pinned to the WINDOW and not to its container: `Toast.raise` moves the stack
   into the open dialog so it sits in front of its backdrop (see ui.js), and
   `position: fixed` is what stops it from ending up in a corner of that dialog
   on the way. */
.toast-stack {
  position: fixed;
  right: var(--space-4);
  bottom: var(--space-4);
  z-index: 60;
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  max-width: 360px;
}

.toast {
  display: flex;
  align-items: flex-start;
  gap: var(--space-2);
  padding: var(--space-3) var(--space-4);
  border: var(--border-width) solid var(--c-border);
  border-radius: var(--radius-md);
  background: var(--c-surface);
  color: var(--c-text);
  box-shadow: var(--shadow-lg);
}

.toast .icon { color: var(--c-primary-text); margin-top: 2px; }
.toast.error .icon { color: var(--c-danger-text); }


/* ---------- Table ---------- */

.table {
  width: 100%;
  border-collapse: collapse;
  font-size: var(--text-sm);
}

.table th,
.table td {
  text-align: left;
  padding: var(--space-3);
  border-bottom: var(--border-width) solid var(--c-border);
}

.table th {
  color: var(--c-text-muted);
  font-size: var(--text-xs);
  font-weight: var(--weight-bold);
  text-transform: uppercase;
  letter-spacing: 0.04em;
}

.table tbody tr:hover { background: var(--c-surface-hover); }

/* Written as .table th/.table td so it outranks the alignment set above,
   instead of leaning on an !important in a utility class. */
.table th.actions,
.table td.actions { text-align: right; white-space: nowrap; }


/* ---------- Tabs ---------- */

.tabs {
  display: flex;
  gap: var(--space-5);
  border-bottom: var(--border-width) solid var(--c-border);
  margin-bottom: var(--space-4);
}

.tab {
  /* A tab can carry an icon: the gap between it and the word belongs to the
     component, not to the page using it. */
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  padding: var(--space-3) 0;
  border: 0;
  border-bottom: 2px solid transparent;
  background: none;
  color: var(--c-text-muted);
  font: inherit;
  font-weight: var(--weight-bold);
  cursor: pointer;
}

.tab:hover { color: var(--c-text); }

.tab[aria-selected="true"] {
  color: var(--c-primary-text);
  border-bottom-color: var(--c-primary-text);
}


/* ---------- Avatar and user chip ---------- */

.avatar {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 40px;
  height: 40px;
  flex: none;
  border-radius: 50%;
  background: var(--c-primary-subtle);
  color: var(--c-primary-text);
  font-weight: var(--weight-bold);
  overflow: hidden;
}

.avatar-sm { width: 28px; height: 28px; font-size: var(--text-xs); }
.avatar-lg { width: 64px; height: 64px; font-size: var(--text-xl); }
.avatar img { width: 100%; height: 100%; object-fit: cover; }

/* The avatar IS the image, not an image placed inside it: a profile photo
   replaces the initials, it does not add to them. `cover` because a portrait in
   a disc must fill it — the one place cropping is what is wanted. */
.avatar-photo { object-fit: cover; }

/* A wrapper for an avatar that has to carry something over it. An <img> has no
   children: the "edit photo" overlay needs a box, and this does nothing else
   than give it one. */
.avatar-frame {
  display: inline-flex;
  flex: none;
  border-radius: var(--radius-pill);
}

/* ============================================================
   Modifier une photo — sur la photo

   L'hôte peut être n'importe quoi qui affiche une image : l'avatar rond du
   profil, le cadre que le CV dessine. Il reçoit `position: relative` et une
   pellicule qui ne se montre qu'au survol ou au focus clavier.

   Rien n'est ajouté à la MISE EN PAGE : la pellicule est en position absolue
   sur toute la surface. Une photo entourée d'un bouton aurait poussé ce qui
   est à côté d'elle, et dans un CV cela aurait déplacé le document.
   ============================================================ */

.has-photo-edit {
  position: relative;
}

/* A descendant selector rather than `.photo-edit` alone: the element also
   carries the .dropdown class (so its menu positions), and .dropdown sets
   `position: relative; display: inline-flex` further down this sheet. At equal
   specificity the last one wins — so this one needs one more, or the overlay
   sits BESIDE the photo instead of covering it. */
.has-photo-edit .photo-edit {
  position: absolute;
  inset: 0;
  display: block;
  /* Passes the host's rounding down to the button, which inherits it in turn:
     a square overlay on a round avatar overflows on four sides. */
  border-radius: inherit;
}

.photo-edit-btn {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 100%;
  height: 100%;
  padding: 0;
  border: 0;
  /* The host's radius, whatever it is: a disc stays a disc. */
  border-radius: inherit;
  background: rgb(0 0 0 / 0.45);
  color: #fff;
  opacity: 0;
  cursor: pointer;
  transition: opacity var(--transition);
}

.has-photo-edit:hover .photo-edit-btn,
.photo-edit-btn:focus-visible {
  opacity: 1;
}

.photo-edit-btn:focus-visible {
  outline: var(--focus-width) solid var(--c-focus);
  outline-offset: var(--focus-offset);
}

/* The menu sits under the photo, left-aligned, and escapes the disc: without
   this the frame's `overflow: hidden` would clip it. */
.has-photo-edit .photo-edit .dropdown-menu {
  top: calc(100% + var(--space-1));
  min-width: 180px;
}

.user-chip {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  color: var(--c-text-muted);
  font-size: var(--text-sm);
}


/* ---------- Topbar ---------- */

.topbar {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  padding: var(--space-2) var(--space-5);
  background: var(--c-surface);
  border-bottom: var(--border-width) solid var(--c-border);
}

.topbar .brand {
  font-size: var(--text-lg);
  font-weight: var(--weight-bold);
  color: var(--c-primary-text);
  text-decoration: none;
}


/* ---------- Modal ---------- */

.modal::backdrop { background: var(--c-overlay); }

.modal {
  width: min(520px, calc(100vw - var(--space-6)));
  padding: 0;
  border: var(--border-width) solid var(--c-border);
  border-radius: var(--radius-lg);
  background: var(--c-surface);
  color: var(--c-text);
  box-shadow: var(--shadow-lg);
}

.modal-header {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  padding: var(--space-4) var(--space-5);
  border-bottom: var(--border-width) solid var(--c-border);
}

.modal-header h2 { margin: 0; flex: 1; }
.modal-body { padding: var(--space-5); }

/* A dialog holding a form rather than a question: wide enough to lay its
   fields out in columns, and scrolling inside itself so the header stays put
   and the two buttons never leave the screen. 80% of the viewport on a
   desktop; on a phone, where 80% would be narrower than the standard dialog,
   the min-width keeps it at the usual full width. */
.modal-wide {
  display: flex;
  flex-direction: column;
  width: min(80vw, 1100px);
  min-width: min(520px, calc(100vw - var(--space-6)));
  max-width: calc(100vw - var(--space-6));
  max-height: calc(100vh - var(--space-6));
}

/* The form is the dialog's only child, so it carries the column layout. */
.modal-wide > form {
  display: flex;
  flex-direction: column;
  min-height: 0;
}

.modal-wide .modal-body {
  flex: 1;
  min-height: 0;
  overflow-y: auto;
}

/* A dialog carrying a LIST, not a form.

   It takes a height decided in advance rather than its content's: a list of
   forty rows capped itself, leaving a short dialog with a scrollbar inside AND
   another around it. Fixed here, the height leaves the body everything the
   header and footer do not take, and it becomes the only thing that scrolls. */
.modal-tall {
  height: min(90vh, calc(100vh - var(--space-6)));
}

.modal-footer {
  display: flex;
  justify-content: flex-end;
  gap: var(--space-2);
  padding: var(--space-4) var(--space-5);
  border-top: var(--border-width) solid var(--c-border);
}

/* The corner opposite the two exit buttons: a side action of the same dialog,
   which is neither what you confirm nor what you back out with. The automatic
   margin pushes it there without changing the others' alignment. */
.modal-footer-lead { margin-right: auto; }


/* ---------- Dropdown ---------- */

.menu {
  min-width: 200px;
  padding: var(--space-1);
  margin: 0;
  list-style: none;
  border: var(--border-width) solid var(--c-border);
  border-radius: var(--radius-md);
  background: var(--c-surface);
  box-shadow: var(--shadow-md);
}

.menu-item {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  width: 100%;
  padding: var(--space-2) var(--space-3);
  border: 0;
  border-radius: var(--radius-sm);
  background: none;
  color: var(--c-text);
  font: inherit;
  font-weight: var(--weight-normal);
  text-align: left;
  text-decoration: none;
  cursor: pointer;
}

.menu-item:hover { background: var(--c-surface-hover); text-decoration: none; }
.menu-item.danger { color: var(--c-danger-text); }
.menu-separator { height: var(--border-width); margin: var(--space-1) 0; background: var(--c-border); }

/* A .menu on its own is a list; hanging it off a trigger needs a positioning
   context, which is all .dropdown is. The menu itself is unchanged. */
.dropdown { position: relative; display: inline-flex; }

.dropdown-menu {
  position: absolute;
  top: calc(100% + var(--space-1));
  right: 0;
  z-index: 40;
}

/* Anchored to the trigger's left edge instead, for a menu that would
   otherwise run off the left of the viewport. */
.dropdown-menu.align-left { right: auto; left: 0; }

/* Opening upwards, for a trigger sitting at the bottom of the window: a menu
   dropping down would have nowhere to unfold. */
.dropdown-menu.drop-up { top: auto; bottom: calc(100% + var(--space-1)); }

/* Non-interactive first row of a menu: who is signed in, which role. */
.menu-heading {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 2px;
  max-width: 240px;
  padding: var(--space-2) var(--space-3);
}

/* An entry that exists but is not available yet. Kept in the menu on purpose:
   removing it would hide that the feature is coming. */
.menu-item[aria-disabled="true"] { color: var(--c-text-muted); cursor: not-allowed; }
.menu-item[aria-disabled="true"]:hover { background: none; }


/* ---------- Breadcrumb and pagination ---------- */

.breadcrumb {
  display: flex;
  align-items: center;
  gap: var(--space-1);
  padding: 0;
  margin: 0 0 var(--space-4);
  list-style: none;
  font-size: var(--text-sm);
  color: var(--c-text-muted);
}

.breadcrumb .icon { color: var(--c-text-muted); }
.breadcrumb [aria-current="page"] { color: var(--c-text); font-weight: var(--weight-bold); }

.pagination { display: flex; align-items: center; gap: var(--space-1); }

.pagination .page-link {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: var(--control-height-sm);
  height: var(--control-height-sm);
  padding: 0 var(--space-2);
  border-radius: var(--radius-sm);
  color: var(--c-text);
  font-weight: var(--weight-normal);
  text-decoration: none;
}

.pagination .page-link:hover { background: var(--c-surface-hover); text-decoration: none; }

.pagination .page-link[aria-current="page"] {
  background: var(--c-primary);
  color: var(--c-primary-on);
  font-weight: var(--weight-bold);
}


/* ---------- Spinner, progress, skeleton ---------- */

.spinner {
  display: inline-block;
  width: 1.25em;
  height: 1.25em;
  border: 2px solid currentColor;
  border-right-color: transparent;
  border-radius: 50%;
  animation: spin 700ms linear infinite;
}

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

.progress {
  width: 100%;
  height: 6px;
  overflow: hidden;
  border-radius: var(--radius-pill);
  background: var(--c-surface-hover);
  border: var(--border-width) solid transparent;
}

.progress-bar { height: 100%; background: var(--c-primary); }

.skeleton {
  border-radius: var(--radius-sm);
  background: linear-gradient(90deg, var(--c-surface-hover) 25%, var(--c-border) 37%, var(--c-surface-hover) 63%);
  background-size: 400% 100%;
  animation: skeleton 1.4s ease infinite;
}

@keyframes skeleton { 0% { background-position: 100% 50%; } 100% { background-position: 0 50%; } }


/* ---------- Tooltip ---------- */

.tooltip { position: relative; display: inline-flex; }

/* Drawn `fixed`, not `absolute`: in absolute it is clipped by the first
   scrolling ancestor — a modal's body, a side panel — and an unreadable
   explanation is worse than a missing one. Its coordinates are set by ui.js on
   hover, in screen pixels. */
.tooltip::after {
  content: attr(data-tooltip);
  position: fixed;
  left: var(--tip-x, 50%);
  top: var(--tip-y, 0);
  transform: translate(-50%, -100%);
  z-index: 50;
  padding: var(--space-1) var(--space-2);
  border-radius: var(--radius-sm);
  background: var(--c-text);
  color: var(--c-bg);
  font-size: var(--text-xs);
  white-space: nowrap;
  opacity: 0;
  pointer-events: none;
  transition: opacity var(--transition);
}

.tooltip:hover::after,
.tooltip:focus-within::after { opacity: 1; }

/* A tooltip that explains rather than labels. The default one is a single
   line by design — a keyboard shortcut, a full name — and a sentence in it
   would run off the screen. */
.tooltip-wide::after {
  width: 240px;
  white-space: normal;
  text-align: left;
  font-weight: var(--weight-normal);
  line-height: var(--leading-base);
}

/* The marker inside a label: right after the words, on the text's baseline,
   and never bold even when the label is. */
.label-help {
  margin-left: var(--space-1);
  vertical-align: text-bottom;
  color: var(--c-text-muted);
  cursor: help;
}

.label-help:focus-visible {
  outline: var(--focus-width) solid var(--c-focus);
  outline-offset: var(--focus-offset);
  border-radius: var(--radius-sm);
}

/* No room above: the tooltip flips below, through the same class ui.js applies
   after measuring. */
.tooltip.tip-below::after { transform: translate(-50%, 0); }


/* ---------- Empty state and divider ---------- */

.empty-state {
  padding: var(--space-7) var(--space-4);
  text-align: center;
  color: var(--c-text-muted);
}

.empty-state .icon-xl { color: var(--c-border-strong); margin-bottom: var(--space-3); }

.divider {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  margin: var(--space-4) 0;
  color: var(--c-text-muted);
  font-size: var(--text-xs);
}

.divider::before,
.divider::after {
  content: "";
  flex: 1;
  height: var(--border-width);
  background: var(--c-border);
}


/* ---------- Code block ---------- */

.code-box {
  padding: var(--space-3);
  border: var(--border-width) solid var(--c-border);
  border-radius: var(--radius-sm);
  background: var(--c-surface-sunken);
  color: var(--c-text);
  font-family: var(--font-mono);
  font-size: var(--text-sm);
  white-space: pre-wrap;
  word-break: break-word;
}


/* ---------- Range ----------
   A slider whose value is shown by whatever sits next to it, never by the
   control: browsers give a range no readable value at all. Pair it with a
   .field-inline label and a .field-value caption.

   The three vendor pseudo-elements are written out separately on purpose.
   Grouping them in one selector list is not a style choice here — an engine
   that does not know one of them drops the whole rule, so the repetition is
   what makes the slider look the same everywhere. */

.range {
  appearance: none;
  -webkit-appearance: none;
  width: 100%;
  height: var(--space-4);
  background: transparent;
  cursor: pointer;
}

.range::-webkit-slider-runnable-track {
  height: 4px;
  border-radius: var(--radius-pill);
  background: var(--c-border);
}

.range::-moz-range-track {
  height: 4px;
  border-radius: var(--radius-pill);
  background: var(--c-border);
}

.range::-webkit-slider-thumb {
  -webkit-appearance: none;
  width: 16px;
  height: 16px;
  /* Half the thumb minus half the track: what centres it on the line. */
  margin-top: -6px;
  border: var(--border-width) solid var(--c-surface);
  border-radius: var(--radius-pill);
  background: var(--c-primary);
}

.range::-moz-range-thumb {
  width: 16px;
  height: 16px;
  border: var(--border-width) solid var(--c-surface);
  border-radius: var(--radius-pill);
  background: var(--c-primary);
}

/* The ring goes around the thumb, not around the full-width track. */
.range:focus-visible { outline: none; }

.range:focus-visible::-webkit-slider-thumb {
  outline: var(--focus-width) solid var(--c-focus);
  outline-offset: var(--focus-offset);
}

.range:focus-visible::-moz-range-thumb {
  outline: var(--focus-width) solid var(--c-focus);
  outline-offset: var(--focus-offset);
}

.range:disabled { cursor: not-allowed; opacity: 0.6; }


/* ---------- Colour input ----------
   The native picker, reduced to a swatch that matches the other controls.
   Always give it a <label>: the colour alone never says what it paints. */

.color-input {
  appearance: none;
  -webkit-appearance: none;
  width: 100%;
  height: var(--control-height-sm);
  padding: 0;
  border: var(--border-width) solid var(--c-border);
  border-radius: var(--radius-sm);
  background: var(--c-surface);
  cursor: pointer;
}

.color-input::-webkit-color-swatch-wrapper { padding: 3px; }
.color-input::-webkit-color-swatch { border: none; border-radius: 2px; }
.color-input::-moz-color-swatch { border: none; border-radius: 2px; }

.color-input:hover { border-color: var(--c-text-muted); }

.color-input:focus-visible {
  outline: var(--focus-width) solid var(--c-focus);
  outline-offset: var(--focus-offset);
}


/* ---------- Compact field rows ----------
   For panels of settings, where a stacked .field per control would run the
   list off the screen: the label keeps its line, the control takes what is
   left, and .field-value reports what a control cannot say by itself. */

.field-inline {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  margin-bottom: var(--space-3);
}

/* A swatch in a compact row keeps a swatch's width: stretched across a
   panel it reads as a coloured bar, not as a control. */
.field-inline > .color-input { flex: 0 0 88px; }

.field-inline > label,
.field-inline > .label {
  flex: 1;
  min-width: 0;
  margin-bottom: 0;
}

.field-value {
  font-size: var(--text-xs);
  color: var(--c-text-muted);
  /* Digits of equal width: a value that changes as a slider moves must not
     make the row jitter. */
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}


/* ============================================================================
   6. UTILITIES
   ========================================================================= */

.muted     { color: var(--c-text-muted); }

/* Semantic text colours, for the times an icon or a word carries the meaning
   without a whole .alert or .badge around it. */
.text-danger  { color: var(--c-danger-text); }
.text-warning { color: var(--c-warning-text); }
.text-success { color: var(--c-success-text); }
.text-info    { color: var(--c-info-text); }

.text-xs   { font-size: var(--text-xs); }
.text-sm   { font-size: var(--text-sm); }
.text-lg   { font-size: var(--text-lg); }
.bold      { font-weight: var(--weight-bold); }
.center    { text-align: center; }
.right     { text-align: right; }
.truncate  { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.full      { width: 100%; }
.no-margin { margin: 0; }

.mt-1 { margin-top: var(--space-1); }
.mt-2 { margin-top: var(--space-2); }
.mt-3 { margin-top: var(--space-3); }
.mt-4 { margin-top: var(--space-4); }
.mt-5 { margin-top: var(--space-5); }
.mb-0 { margin-bottom: 0; }
.mb-3 { margin-bottom: var(--space-3); }
.mb-5 { margin-bottom: var(--space-5); }

/* Visible to screen readers only */
.sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0 0 0 0);
  white-space: nowrap;
  border: 0;
}


/* ---------- Drag-and-drop ordered lists ---------- */

/* The grip. A button, so keyboard-reachable: the arrows move the row, which
   drag and drop will never do. */
.drag-grip {
  flex: none;
  display: flex;
  align-items: center;
  padding: var(--space-1);
  border: 0;
  border-radius: var(--radius-sm);
  background: none;
  color: var(--c-text-muted);
  cursor: grab;
}

.drag-grip:hover { color: var(--c-text); }

.drag-grip:focus-visible {
  outline: var(--focus-width) solid var(--c-focus);
  outline-offset: var(--focus-offset);
}

.is-dragging {
  opacity: 0.4;
  cursor: grabbing;
}

/* The insertion line: WHICH SIDE the row will land on. Simply highlighting the
   targeted row did not say it — over a gap, "that row" does not answer "before
   or after?". */
.is-drop-before { box-shadow: inset 0 3px 0 0 var(--c-primary); }
.is-drop-after { box-shadow: inset 0 -3px 0 0 var(--c-primary); }

/* An EMPTY list of a drop group: there is no row to underline, so the list
   itself is outlined. Without it, the one target that cannot be seen would be
   the one aimed at most often — the category just created. */
.is-drop-into { box-shadow: inset 0 0 0 2px var(--c-primary); }
