/* hanif ui 4.3.0 — https://ui.hanif.app
   The interface system for the apps on *.hanif.app.
   This file combines tokens.css + ui.css + layout.css.
   The rules are at https://ui.hanif.app/llms.txt */

/* ─────────────────────────────────────────────────────────────────────────
   hanif ui · tokens

   THE IDEA: THE CHROME STEPS BACK.

   This system follows how foto.hanif.app is put together, and the reason is
   not taste — it is how the eye works. In a photo app, chrome with a colour
   of its own takes away the eye's reference: a slightly warm photo looks
   neutral on a blue background. Lightroom, Capture One and Darkroom are all
   grey for that reason. It is a requirement, not a lack of ideas.

   The general rule that falls out of it: THE CONTENT MAY HAVE COLOUR, THE
   CONTAINER MAY NOT. Numbers, charts, photos, people's words — that is
   content. The panels, bars, cards and sidebars that hold them stay quiet.

   Three rules follow from that:

   1. ONE ACCENT PER THEME, and it appears in three places only: keyboard
      focus, selection, and the primary button. Everywhere else is neutral.
   2. TEXTURE FROM GRAIN, NOT GRADIENTS. A flat field of colour looks dead,
      but a gradient gives the chrome a hue of its own. Grain gives it a
      material without a colour. See --grain / --grain-blend.
   3. DEPTH FROM HAIRLINES AND THIN SHADOWS, NOT GLOW. Things that float get
      a layered shadow (--shadow-s/m/l); things that do not float get one
      1px line.

   THE THEME BELONGS TO THE READER, NOT THE AUTHOR. There are five studios —
   Paper, Mist, Sepia, Graphite, Ink — chosen with data-studio on <html>.
   The dark ones are soft black on purpose, never pure black: pure black
   makes the edges of content glare and tires the eye fast.

   THE OBJECT COLOURS ARE STILL HERE. Six of them (denim, tomato, grass,
   mustard, plum, sea) were not dropped — their job was made clear: they
   mark DATA and STATUS (chart series, category labels, danger levels), not
   walls. What was banned stays banned: decorative gradients, glow, rainbow
   text.

   Everything that changes with screen width is settled HERE with clamp(),
   so components never need breakpoints of their own. The system has four
   breakpoints and no more: 480 · 640 · 900 · 1200.
   ───────────────────────────────────────────────────────────────────────── */

/* ══ STUDIO PAPER (default) ═══════════════════════════════════════════════
   Warm neutral, like printed paper that has been in a drawer a while. It is
   the default because it is the kindest to read for a long time, and the
   most honest when there are pictures on it. */
:root {
  color-scheme: light;

  /* ── Surfaces ────────────────────────────────────────────────────────
     Three levels, and the order matters: --paper is the CANVAS (the page
     background), --surface is what RISES above it (cards, panels, menus,
     inputs), --sunk is what SITS INSIDE it (code blocks, tracks, wells).

     The earlier version used one white for both the page and the cards, and
     told them apart with a 2px line. That works, but the result reads like
     a technical drawing. Once a card gets its own surface, lighter than its
     canvas, depth comes from light — and the line can go back to being a
     hairline. */
  --paper:   #f4f3ef;
  --surface: #fefdfb;
  --sunk:    #ebe9e3;
  --sunk-2:  #e2dfd8;

  /* Old names kept so apps that copied an earlier version do not break.
     --paper-2 used to mean "a little darker than the page", which is
     exactly what --sunk means now. */
  --paper-2: var(--sunk);
  --paper-3: var(--sunk-2);

  /* ── Ink ─────────────────────────────────────────────────────────────
     Three levels, all measured against the LIGHTEST surface in this theme —
     not just against the canvas. Small 13px text sitting on a card passes
     the check easily if you only test it on one background, and then goes
     faint on another. */
  --ink:   #17150f;
  --ink-2: #56514a;
  --ink-3: #6e6860;

  /* Hairlines. Written as transparent colours, not solid greys, so they
     stay correct on the canvas and on the lighter card alike. */
  --line:   rgb(23 21 15 / 0.13);
  --line-2: rgb(23 21 15 / 0.06);

  /* ── Accent ──────────────────────────────────────────────────────────
     Teal. It reads as fast and soft — cool enough not to fight warm
     content, solid enough to hold white text. Change the whole system in
     one line: :root { --accent: var(--plum) } */
  --accent:      #0e6e64;
  --on-accent:   #ffffff;
  --accent-ink:  var(--on-accent);   /* the name foto.hanif.app uses */
  --accent-soft: rgb(14 110 100 / 0.10);
  --accent-line: color-mix(in srgb, var(--accent) 34%, var(--line));
  --accent-dark: color-mix(in srgb, var(--accent) 74%, var(--ink));

  /* The mat behind media. Kept apart from --paper because a picture needs a
     backing slightly darker than the canvas for its edges to read — and so
     it can be swapped on its own without changing the theme. */
  --mat: #d8d5cf;

  /* ── Grain ───────────────────────────────────────────────────────────
     This is what replaces gradients. The value is small on purpose: if you
     can see it as an effect, it is already too much. In light themes the
     grain multiplies (darkens a touch); in dark themes it screens (lightens
     a touch) — the right direction for each. */
  --grain: 0.03;
  --grain-blend: multiply;

  /* ── Object colours ──────────────────────────────────────────────────
     No colour here is purely digital. Each one is tilted towards something
     with a real origin — jeans washed many times, a tomato, dry grass,
     mustard, a plum, shallow sea. Anything straight out of a colour picker
     (#0000ff, #00ff00) is banned: that is a colour a machine chose, and it
     shows.

     Their job is now clear: MARK THE DATA, do not paint the walls. Chart
     series, category labels, danger levels — and, where it is really
     needed, one alternate accent per app. */
  --denim:   #2f5496;  --on-denim:   #ffffff;
  --tomato:  #d1442a;  --on-tomato:  #ffffff;
  --grass:   #3d7a3a;  --on-grass:   #ffffff;
  --mustard: #cf9209;  --on-mustard: #17150f;
  --plum:    #6d3f80;  --on-plum:    #ffffff;
  --sea:     #0e6e64;  --on-sea:     #ffffff;

  /* Older names, kept alive as aliases. */
  --blue:   var(--denim);   --on-blue:   var(--on-denim);
  --orange: var(--tomato);  --on-orange: var(--on-tomato);
  --green:  var(--grass);   --on-green:  var(--on-grass);
  --red:    var(--tomato);  --on-red:    var(--on-tomato);
  --yellow: var(--mustard); --on-yellow: var(--on-mustard);
  --purple: var(--plum);    --on-purple: var(--on-plum);

  /* Danger gets its own slot so each studio can correct it — a red that is
     right on paper is too dark on ink. */
  --danger: #9a3324;

  /* ── Type ────────────────────────────────────────────────────────────
     Bricolage Grotesque: a face that is a little wide, with cuts that are
     not stiff. That is what keeps the interface from feeling cold even
     though its colours are deliberately quiet — which is exactly what a
     system that mutes its own colour needs. Optical sizing is on, so small
     sizes loosen themselves and stay readable.

     The font is OPTIONAL, and that is on purpose: the first name in the
     list is only used if it is there. An app that promises zero outside
     requests can copy this file and load no font at all — the system falls
     back to the device face, and not one size shifts.

     Numbers and metadata stay monospace. In any list, a column of equal
     width is far easier to scan than proportional figures. */
  --sans: "Bricolage Grotesque", -apple-system, BlinkMacSystemFont, "Inter", "Segoe UI", system-ui, sans-serif;
  --mono: "IBM Plex Mono", ui-monospace, SFMono-Regular, "SF Mono", Menlo, monospace;
  --font-ui: var(--sans);      /* the names foto.hanif.app uses */
  --font-mono: var(--mono);

  /* Sizes are one step larger than the web habit: body 17px, label 15px,
     smallest 13px. Shrinking text is the easiest way to make a page look
     "dense and professional", and the easiest way to make people squint.
     Anything that needs real app density uses [data-density="compact"]
     below — not small font sizes written one component at a time. */
  --t-xs: 0.8125rem;   /* 13 */
  --t-sm: 0.9375rem;   /* 15 */
  --t-md: 1.0625rem;   /* 17 */
  --t-lg: clamp(1.1875rem, 1.14rem + 0.22vw, 1.3125rem);
  --t-xl: clamp(1.375rem, 1.27rem + 0.5vw, 1.625rem);
  --t-2xl: clamp(1.625rem, 1.4rem + 1.02vw, 2.125rem);
  --t-3xl: clamp(2.125rem, 1.79rem + 1.46vw, 2.875rem);
  --t-display: clamp(2.625rem, 1.6rem + 5.1vw, 5rem);

  /* Metadata size: for mono labels standing over a number or at the head of
     a column. Below 11px a mono face starts to fall apart. */
  --t-meta: 0.6875rem; /* 11 */

  /* ── Space ───────────────────────────────────────────────────────────
     The first six are fixed: those are distances INSIDE a component. The
     last three are the gaps between big sections, and those do have to
     shrink on a phone. */
  --s1: 4px;  --s2: 8px;  --s3: 14px; --s4: 20px;
  --s5: 28px; --s6: 40px;
  --s7: clamp(32px, 5.5vw, 56px);
  --s8: clamp(44px, 8vw, 76px);
  --s9: clamp(56px, 11vw, 112px);

  /* ── Corners ─────────────────────────────────────────────────────────
     Round enough to feel soft, not so round that a small field reads as a
     sticker. Four steps, and the largest is only for large surfaces —
     panels, dialogs, cover images — never for a chip. */
  --r-sm:   5px;
  --r-btn:  9px;
  --r-card: 14px;
  --r-lg:   18px;
  --r-pill: 999px;

  /* ── Lines ───────────────────────────────────────────────────────────
     A hairline by default. The 2px one was not deleted — it moved to where
     a line really has to read as drawn rather than as an edge: table heads,
     the underside of a button, the marker on an active column. */
  --b:       1px;
  --b-thick: 2px;

  /* Page shells */
  --w-wide:    1760px;
  --w-dash:    1560px;
  --w-compact: 460px;
  --w-prose:   66ch;
  --gutter:    clamp(20px, 4vw, 56px);

  --h-topbar: 60px;

  /* Safe edges — notch, rounded corners, the iPhone home line. */
  --safe-t: env(safe-area-inset-top, 0px);
  --safe-b: env(safe-area-inset-bottom, 0px);
  --safe-l: env(safe-area-inset-left, 0px);
  --safe-r: env(safe-area-inset-right, 0px);

  --z-side:    10;
  --z-topbar:  20;
  --z-pop:     30;
  --z-overlay: 50;
  --z-toast:   60;
  --z-grain:   9999;

  /* ── Shadows ─────────────────────────────────────────────────────────
     Three levels, each one two layers: a tight 1-2px shadow that seats the
     object on its surface, and a wide, very thin one that gives it
     distance. A single shadow always reads as a sticker stuck on top; two
     layers read as an object floating.
     --shadow-s: things resting (cards, inputs)
     --shadow-m: things lifted (a hovered card, a menu)
     --shadow-l: things floating over everything (dialogs, drawers) */
  --shadow-s: 0 1px 2px rgb(23 21 15 / 0.06);
  --shadow-m: 0 1px 2px rgb(23 21 15 / 0.06), 0 6px 16px rgb(23 21 15 / 0.08);
  --shadow-l: 0 2px 6px rgb(23 21 15 / 0.08), 0 20px 48px rgb(23 21 15 / 0.16);

  /* Old names. */
  --shadow-1: var(--shadow-s);
  --shadow-2: var(--shadow-l);

  /* Glass: for bars and panels that sit over scrolling content. The blur is
     small on purpose — its job is to stop the text behind from competing,
     not to become the view itself. */
  --blur: 12px;

  /* ── Motion ──────────────────────────────────────────────────────────
     The foto.hanif.app curve: leaves quickly, lands slowly, never bounces.
     Things on this screen are objects, not balloons. */
  --ease:    cubic-bezier(0.2, 0, 0, 1);
  --ease-in: cubic-bezier(0.65, 0, 0.35, 1);
  --d-fast:  90ms;
  --d-mid:   180ms;
}

@media (min-width: 900px) {
  :root { --h-topbar: 68px; }
}

/* ══ STUDIO PAPER, NAMED ══════════════════════════════════════════════════
   The same values as :root above, written out again under the attribute.
   This is not repetition for its own sake: without it, data-studio="paper"
   on a card INSIDE a dark page would find no rule of its own and inherit the
   dark values from the page — a "Paper" card that renders in Ink. On <html>
   the attribute already wins through the :not([data-studio]) guard below;
   this block is what makes it win on any smaller element too. */
[data-studio="paper"] {
  color-scheme: light;
  --paper:   #f4f3ef;
  --surface: #fefdfb;
  --sunk:    #ebe9e3;
  --sunk-2:  #e2dfd8;
  --ink:     #17150f;
  --ink-2:   #56514a;
  --ink-3:   #6e6860;
  --line:    rgb(23 21 15 / 0.13);
  --line-2:  rgb(23 21 15 / 0.06);
  --accent:  #0e6e64;
  --on-accent: #ffffff;
  --accent-soft: rgb(14 110 100 / 0.10);
  --accent-dark: color-mix(in srgb, var(--accent) 74%, var(--ink));
  --danger:  #9a3324;
  --mat:     #d8d5cf;
  --grain:   0.03;
  --grain-blend: multiply;
  --shadow-s: 0 1px 2px rgb(23 21 15 / 0.06);
  --shadow-m: 0 1px 2px rgb(23 21 15 / 0.06), 0 6px 16px rgb(23 21 15 / 0.08);
  --shadow-l: 0 2px 6px rgb(23 21 15 / 0.08), 0 20px 48px rgb(23 21 15 / 0.16);
  --denim:   #2f5496;  --on-denim:   #ffffff;
  --tomato:  #d1442a;  --on-tomato:  #ffffff;
  --grass:   #3d7a3a;  --on-grass:   #ffffff;
  --mustard: #cf9209;  --on-mustard: #17150f;
  --plum:    #6d3f80;  --on-plum:    #ffffff;
  --sea:     #0e6e64;  --on-sea:     #ffffff;
}

/* ══ STUDIO MIST ══════════════════════════════════════════════════════════
   Cool grey — the most neutral of them all, and the safest reference when
   what you are judging on screen is the colour of something. */
[data-studio="mist"] {
  color-scheme: light;
  --paper:   #eff1f3;
  --surface: #ffffff;
  --sunk:    #e4e7ea;
  --sunk-2:  #dadee2;
  --ink:     #14171a;
  --ink-2:   #4c535a;
  --ink-3:   #606870;
  --line:    rgb(20 23 26 / 0.13);
  --line-2:  rgb(20 23 26 / 0.06);
  --accent:  #0e6e64;
  --on-accent: #ffffff;
  --accent-soft: rgb(14 110 100 / 0.10);
  --danger:  #93341f;
  --mat:     #cfd4d9;
  --mustard: #cf9209;  --on-mustard: #14171a;
}

/* ══ STUDIO SEPIA ═════════════════════════════════════════════════════════
   A warm darkroom mood next to a cool signal. Warm against cool is the
   point here — that is what keeps the accent reading as a signal rather
   than as part of the mood. A mood, not a costume: this is a shifted colour
   temperature, not a period piece. */
[data-studio="sepia"] {
  color-scheme: light;
  --paper:   #f1eadf;
  --surface: #fbf6ee;
  --sunk:    #e6ddcf;
  --sunk-2:  #ded3c2;
  --ink:     #23190e;
  --ink-2:   #5c4c38;
  --ink-3:   #6f5d47;
  --line:    rgb(35 25 14 / 0.14);
  --line-2:  rgb(35 25 14 / 0.07);
  --accent:  #12695c;
  --on-accent: #ffffff;
  --accent-soft: rgb(18 105 92 / 0.11);
  --danger:  #8f3520;
  --mat:     #d5c8b4;
  --grain:   0.035;
  --mustard: #b57f07;  --on-mustard: #ffffff;
}

/* ══ STUDIO GRAPHITE ══════════════════════════════════════════════════════
   Dark mid grey. This is the long-shift theme: dark enough not to glare in
   a dim room, light enough to keep the soft shadows between panels. */
[data-studio="graphite"] {
  color-scheme: dark;
  --paper:   #1f2225;
  --surface: #282b2f;
  --sunk:    #16181a;
  --sunk-2:  #101214;
  --ink:     #eceae7;
  --ink-2:   #aca9a3;
  /* Raised from #918e88: that value was only 4.35:1 against --surface —
     under the 4.5 AA threshold for small text, and --ink-3 is exactly the
     token that carries 11px labels. Measured against the LIGHTEST surface
     in this studio, not against its canvas. */
  --ink-3:   #97948e;
  --line:    rgb(255 255 255 / 0.12);
  --line-2:  rgb(255 255 255 / 0.06);
  --accent:  #4fbcad;
  --on-accent: #0e1b18;
  --accent-soft: rgb(79 188 173 / 0.15);
  --accent-dark: color-mix(in srgb, var(--accent) 68%, #000);
  --danger:  #e08070;
  --mat:     #35383c;

  --grain: 0.045;
  --grain-blend: screen;

  --shadow-s: 0 1px 2px rgb(0 0 0 / 0.4);
  --shadow-m: 0 2px 6px rgb(0 0 0 / 0.45), 0 10px 24px rgb(0 0 0 / 0.35);
  --shadow-l: 0 4px 12px rgb(0 0 0 / 0.5), 0 28px 64px rgb(0 0 0 / 0.5);

  /* The object colours are lightened to stay bold on a dark ground, not
     dimmed into something wistful. Their text pairs flip to dark. */
  --denim:   #6a94e0;  --on-denim:   #0e1b18;
  --tomato:  #f0674a;  --on-tomato:  #0e1b18;
  --grass:   #62ac5c;  --on-grass:   #0e1b18;
  --mustard: #e8b431;  --on-mustard: #0e1b18;
  --plum:    #ad78c4;  --on-plum:    #0e1b18;
  --sea:     #4fbcad;  --on-sea:     #0e1b18;
}

/* ══ STUDIO INK ═══════════════════════════════════════════════════════════
   Soft black, NOT pure black. Pure black (#000) makes the edges of content
   glare and tires the eye quickly in a dark room; #161819 gives the same
   darkness without that. */
[data-studio="ink"] {
  color-scheme: dark;
  --paper:   #161819;
  --surface: #1e2123;
  --sunk:    #0f1112;
  --sunk-2:  #0a0c0d;
  --ink:     #edecea;
  --ink-2:   #a4a29d;
  /* Raised from #86847e for the same reason as Graphite: 4.33:1 against
     --surface, while this token carries the 11px text. */
  --ink-3:   #8c8a84;
  --line:    rgb(255 255 255 / 0.10);
  --line-2:  rgb(255 255 255 / 0.05);
  --accent:  #57c4b5;
  --on-accent: #0e1b18;
  --accent-soft: rgb(87 196 181 / 0.15);
  --accent-dark: color-mix(in srgb, var(--accent) 68%, #000);
  --danger:  #e08070;
  --mat:     #1a1c1e;

  --grain: 0.05;
  --grain-blend: screen;

  --shadow-s: 0 1px 2px rgb(0 0 0 / 0.5);
  --shadow-m: 0 2px 8px rgb(0 0 0 / 0.5), 0 12px 28px rgb(0 0 0 / 0.4);
  --shadow-l: 0 4px 16px rgb(0 0 0 / 0.6), 0 32px 72px rgb(0 0 0 / 0.55);

  --denim:   #6a94e0;  --on-denim:   #0e1b18;
  --tomato:  #f0674a;  --on-tomato:  #0e1b18;
  --grass:   #62ac5c;  --on-grass:   #0e1b18;
  --mustard: #e8b431;  --on-mustard: #0e1b18;
  --plum:    #ad78c4;  --on-plum:    #0e1b18;
  --sea:     #57c4b5;  --on-sea:     #0e1b18;
}

/* ══ DARK WITHOUT JAVASCRIPT ══════════════════════════════════════════════
   Written twice on purpose: once following the device setting (so an app
   that only copies this file, with no JavaScript at all, still gets dark),
   and once through data-theme (so a theme button wins in both directions).
   The :not([data-theme="light"]) guard is what keeps the two in agreement.

   The default dark is Ink. The :not([data-studio]) guard matters: the
   moment someone picks a studio themselves, their choice wins — including
   picking a light studio on a device that is set to dark. */
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]):not([data-studio]) {
    color-scheme: dark;
    --paper:   #161819;
    --surface: #1e2123;
    --sunk:    #0f1112;
    --sunk-2:  #0a0c0d;
    --ink:     #edecea;
    --ink-2:   #a4a29d;
    --ink-3:   #8c8a84;
    --line:    rgb(255 255 255 / 0.10);
    --line-2:  rgb(255 255 255 / 0.05);
    --accent:  #57c4b5;
    --on-accent: #0e1b18;
    --accent-soft: rgb(87 196 181 / 0.15);
    --accent-dark: color-mix(in srgb, var(--accent) 68%, #000);
    --danger:  #e08070;
    --mat:     #1a1c1e;

    --grain: 0.05;
    --grain-blend: screen;

    --shadow-s: 0 1px 2px rgb(0 0 0 / 0.5);
    --shadow-m: 0 2px 8px rgb(0 0 0 / 0.5), 0 12px 28px rgb(0 0 0 / 0.4);
    --shadow-l: 0 4px 16px rgb(0 0 0 / 0.6), 0 32px 72px rgb(0 0 0 / 0.55);

    --denim:   #6a94e0;  --on-denim:   #0e1b18;
    --tomato:  #f0674a;  --on-tomato:  #0e1b18;
    --grass:   #62ac5c;  --on-grass:   #0e1b18;
    --mustard: #e8b431;  --on-mustard: #0e1b18;
    --plum:    #ad78c4;  --on-plum:    #0e1b18;
    --sea:     #57c4b5;  --on-sea:     #0e1b18;
  }
}

/* data-theme="dark" with no studio = Ink. This is the compatibility path
   for apps already using the earlier light/dark button. */
[data-theme="dark"]:not([data-studio]) {
  color-scheme: dark;
  --paper:   #161819;
  --surface: #1e2123;
  --sunk:    #0f1112;
  --sunk-2:  #0a0c0d;
  --ink:     #edecea;
  --ink-2:   #a4a29d;
  --ink-3:   #8c8a84;
  --line:    rgb(255 255 255 / 0.10);
  --line-2:  rgb(255 255 255 / 0.05);
  --accent:  #57c4b5;
  --on-accent: #0e1b18;
  --accent-soft: rgb(87 196 181 / 0.15);
  --accent-dark: color-mix(in srgb, var(--accent) 68%, #000);
  --danger:  #e08070;
  --mat:     #1a1c1e;

  --grain: 0.05;
  --grain-blend: screen;

  --shadow-s: 0 1px 2px rgb(0 0 0 / 0.5);
  --shadow-m: 0 2px 8px rgb(0 0 0 / 0.5), 0 12px 28px rgb(0 0 0 / 0.4);
  --shadow-l: 0 4px 16px rgb(0 0 0 / 0.6), 0 32px 72px rgb(0 0 0 / 0.55);

  --denim:   #6a94e0;  --on-denim:   #0e1b18;
  --tomato:  #f0674a;  --on-tomato:  #0e1b18;
  --grass:   #62ac5c;  --on-grass:   #0e1b18;
  --mustard: #e8b431;  --on-mustard: #0e1b18;
  --plum:    #ad78c4;  --on-plum:    #0e1b18;
  --sea:     #57c4b5;  --on-sea:     #0e1b18;
}

/* ══ APP DENSITY ══════════════════════════════════════════════════════════
   A page you read and an app you work in do not need the same sizes. What
   you read wants large type and room; what you work in wants more rows on
   screen without scrolling.

   This is the only correct way to make things denser: lower the scale in
   one place, never write small font sizes component by component. Put it on
   <html> or on any wrapper: <div data-density="compact">. Only sizes
   change — colour, shape and the gaps between sections stay put. */
[data-density="compact"] {
  --t-xs: 0.75rem;     /* 12 */
  --t-sm: 0.8125rem;   /* 13 */
  --t-md: 0.875rem;    /* 14 */
  --t-lg: 1rem;        /* 16 */
  --t-xl: 1.25rem;     /* 20 */
  --t-2xl: 1.625rem;   /* 26 */
  --t-3xl: 2.125rem;   /* 34 */
  --t-meta: 0.6875rem; /* 11 */

  --s3: 12px; --s4: 16px; --s5: 24px; --s6: 32px;
  --s7: clamp(24px, 4vw, 40px);
  --s8: clamp(32px, 6vw, 56px);
  --s9: clamp(40px, 8vw, 72px);

  --h-topbar: 58px;
}


/* ─────────────────────────────────────────────────────────────────────────
   hanif ui · components
   The rule: neutral surfaces bounded by hairlines, colour only where
   something must be pressed or marked. If one screen has more than one
   full field of colour, one of them is not important.

   On narrow screens: almost every adjustment is already settled in the
   tokens (clamp) and in lib/layout.css. What is left here is only what has
   to CHANGE SHAPE, not merely shrink — a table becoming cards, a dialog
   becoming a bottom sheet, a drawer going full width. Those are written
   next to their own component, not piled at the end of the file.
   ───────────────────────────────────────────────────────────────────────── */

*, *::before, *::after { box-sizing: border-box; margin: 0; }
html { -webkit-text-size-adjust: 100%; scroll-behavior: smooth; }
html, body { overflow-x: clip; }
body {
  background: var(--paper);
  color: var(--ink);
  font: var(--t-md)/1.55 var(--sans);
  font-optical-sizing: auto;
  -webkit-font-smoothing: antialiased;
  letter-spacing: -0.005em;
  min-height: 100dvh;
}
@media (prefers-reduced-motion: reduce) { html { scroll-behavior: auto; } }

/* A studio set on something smaller than <html> — a card, a panel, a demo —
   must re-read its ink. `color` is inherited as a RESOLVED value, not as
   var(--ink): a card marked data-studio="mist" inside a dark page would get
   new variables but keep the page's light text, and its own text would
   vanish into its own light surface. One line makes any scoped studio
   self-contained. */
[data-studio], [data-theme] { color: var(--ink); }

/* ── Grain ────────────────────────────────────────────────────────────────
   One layer of fine noise over the whole page. This is what replaces
   gradients: a flat field of colour looks dead, but a gradient gives the
   chrome a hue of its own — and once the container has a colour, the eye
   loses its reference for judging the colour of the content. Grain gives a
   material without carrying any colour.

   Its strength is set per studio through --grain (0.03 light, 0.05 dark)
   and its direction through --grain-blend: multiply in light themes so it
   darkens a touch, screen in dark themes so it lightens a touch. If you can
   see the grain as an "effect", the value is too high.

   It lives on body::after so it needs no extra element, and its
   pointer-events are off so it never catches a click. */
body::after {
  content: "";
  position: fixed;
  inset: 0;
  z-index: var(--z-grain);
  pointer-events: none;
  opacity: var(--grain);
  mix-blend-mode: var(--grain-blend);
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='160' height='160'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.8' numOctaves='4'/%3E%3C/filter%3E%3Crect width='160' height='160' filter='url(%23n)'/%3E%3C/svg%3E");
}
/* A page that does not want grain — say, one holding a single image canvas —
   turns it off in one place: <body data-grain="off">. */
body[data-grain="off"]::after { display: none; }

a { color: inherit; text-decoration-thickness: 1px; text-underline-offset: 3px; }
button, input, select, textarea { font: inherit; color: inherit; }
img, svg, video, canvas { max-width: 100%; height: auto; }
/* The built-in HTML `hidden` attribute is only `display:none` at specificity
   (0,1,0) — so a rule as simple as `.side a { display:block }` (0,2,0) beats
   it, and an element already marked hidden stays visible. This is the trap
   that makes list filtering fail silently: the JavaScript is right, the
   attribute is set, and nothing changes on screen. This one line closes the
   whole class of problem. */
[hidden] { display: none !important; }
/* The same problem exactly, and more dangerous because it looks like only
   one component is broken: a closed <dialog> is hidden by the browser's own
   stylesheet, not by anything we wrote. The moment a rule gives it an
   explicit `display` — say `.command{display:flex}` so max-height works —
   that rule wins, and a dialog that was never opened shows up at the bottom
   of the page as ordinary content. These two lines put the rule back: what
   decides visibility is the `open` attribute, not whoever wrote `display`
   last. */
dialog:not([open]) { display: none !important; }
dialog[open] { display: block; }
dialog.command[open] { display: flex; }
dialog.sheet[open], dialog.sheet-bottom[open] { display: block; }
:focus-visible { outline: 3px solid var(--accent); outline-offset: 2px; border-radius: 4px; }
::selection { background: var(--accent); color: var(--on-accent); }
/* An anchor must not slide under a bar that sticks */
:target { scroll-margin-top: calc(var(--h-topbar) + var(--s5)); }

/* ── Icons ────────────────────────────────────────────────────────────────
   Drawn by hand in lib/icons.svg, never emoji. They size with the text next
   to them and take their colour from it, so they are genuinely part of the
   sentence — not a sticker pasted beside it.
     <svg class="ico" aria-hidden="true"><use href="/lib/icons.svg#search"/></svg> */
.ico { width: 1.15em; height: 1.15em; flex: none; vertical-align: -0.18em; }
.ico-lg { width: 1.5em; height: 1.5em; }
.ico-xl { width: 2.25em; height: 2.25em; }

/* ── Text ─────────────────────────────────────────────────────────────── */
.display { font-size: var(--t-display); font-weight: 680; line-height: 0.96; letter-spacing: -0.035em; }
.h1 { font-size: var(--t-3xl); font-weight: 650; line-height: 1.06; letter-spacing: -0.03em; }
.h2 { font-size: var(--t-2xl); font-weight: 640; line-height: 1.12; letter-spacing: -0.025em; }
.h3 { font-size: var(--t-xl); font-weight: 620; line-height: 1.2; letter-spacing: -0.02em; }
.lead { font-size: var(--t-lg); color: var(--ink-2); line-height: 1.5; }
.small { font-size: var(--t-sm); }
.muted { color: var(--ink-2); }
.faint { color: var(--ink-3); }
.mono { font-family: var(--mono); font-variant-numeric: tabular-nums; }
.eyebrow {
  font-size: var(--t-xs); font-weight: 640; letter-spacing: 0.08em;
  text-transform: uppercase; color: var(--accent);
}
/* ── Metadata ─────────────────────────────────────────────────────────────
   Small labels standing over a number, at the head of a column, or under a
   picture: monospace, uppercase, tracked out. This is not a decoration —
   monospace makes a column of figures equal in width and far easier to
   scan, and once the label is monospace too, label and number read as one
   family. Below 11px a mono face starts to fall apart, so that is the
   floor. */
.meta {
  font: 500 var(--t-meta)/1.45 var(--mono);
  letter-spacing: 0.08em; text-transform: uppercase; color: var(--ink-3);
}
.meta-ink { color: var(--ink-2); }
/* Long headings must not spill off a narrow screen */
.display, .h1, .h2, .h3 { overflow-wrap: break-word; text-wrap: balance; }
.lead, p { overflow-wrap: break-word; }

/* ── Buttons ──────────────────────────────────────────────────────────────
   The shape has shoulders on purpose: round enough to feel friendly, never
   so round it becomes a pill. The fill colour is used solid — one primary
   button per screen, the rest outlined or plain. */
.btn {
  --btn-bg: var(--ink);
  --btn-fg: var(--surface);
  --btn-edge: color-mix(in srgb, var(--btn-bg) 72%, #000);
  position: relative;
  display: inline-flex; align-items: center; justify-content: center; gap: var(--s2);
  min-height: 46px; padding: 0 var(--s5);
  border: 0; border-bottom: 3px solid var(--btn-edge);
  border-radius: var(--r-btn);
  background: var(--btn-bg); color: var(--btn-fg);
  font-size: var(--t-sm); font-weight: 620; letter-spacing: -0.005em;
  cursor: pointer; text-decoration: none; white-space: nowrap;
  -webkit-tap-highlight-color: transparent;
  touch-action: manipulation;
  transition: transform var(--d-fast) var(--ease), border-bottom-width var(--d-fast) var(--ease),
              background var(--d-fast) var(--ease), box-shadow var(--d-fast) var(--ease);
}
/* This is the signature of the system: the button has THICKNESS. Its
   underside is 3px of the same colour, dropped, and when pressed that edge
   thins while the button sinks — exactly like a button that really goes in.
   Not a shadow, not a glow: one line that is honest about depth.
   It is also the only motion left in this component. */
.btn:active { transform: translateY(2px); border-bottom-width: 1px; }
/* Hover is only for people with a mouse, and it is only colour — nothing
   jumps. On a touch screen :hover sticks after the finger lifts. */
@media (hover: hover) {
  .btn:hover { background: color-mix(in srgb, var(--btn-bg) 88%, var(--ink)); }
}
.btn:disabled, .btn[aria-disabled="true"] { opacity: 0.4; pointer-events: none; }

.btn-accent  { --btn-bg: var(--accent);  --btn-fg: var(--on-accent); }
.btn-denim   { --btn-bg: var(--denim);   --btn-fg: var(--on-denim); }
.btn-tomato   { --btn-bg: var(--tomato);   --btn-fg: var(--on-tomato); }
.btn-grass  { --btn-bg: var(--grass);  --btn-fg: var(--on-grass); }
.btn-mustard { --btn-bg: var(--mustard); --btn-fg: var(--on-mustard); }
.btn-plum    { --btn-bg: var(--plum);    --btn-fg: var(--on-plum); }
.btn-sea    { --btn-bg: var(--sea);    --btn-fg: var(--on-sea); }
/* old names, kept alive for apps that copied an earlier version */
.btn-blue { --btn-bg: var(--denim); --btn-fg: var(--on-denim); }
.btn-orange, .btn-red { --btn-bg: var(--tomato); --btn-fg: var(--on-tomato); }
.btn-green { --btn-bg: var(--grass); --btn-fg: var(--on-grass); }
.btn-yellow { --btn-bg: var(--mustard); --btn-fg: var(--on-mustard); }
.btn-purple { --btn-bg: var(--plum); --btn-fg: var(--on-plum); }

/* The ones with no fill still have thickness — their edge is drawn from the
   border, not from a fill colour that is not there. */
.btn-outline {
  --btn-bg: transparent; --btn-fg: var(--ink); --btn-edge: var(--line-2);
  box-shadow: inset 0 0 0 var(--b) var(--line-2);
}
.btn-ghost { --btn-bg: transparent; --btn-fg: var(--ink-2); --btn-edge: transparent; }
@media (hover: hover) {
  .btn-outline:hover { background: var(--paper-2); box-shadow: inset 0 0 0 var(--b) var(--ink); }
  .btn-ghost:hover { background: var(--paper-2); color: var(--ink); }
}

.btn-lg { min-height: 58px; padding: 0 var(--s6); font-size: var(--t-md); border-radius: var(--r-card); border-bottom-width: 4px; }
.btn-lg:active { transform: translateY(3px); border-bottom-width: 1px; }
.btn-sm { min-height: 36px; padding: 0 var(--s3); font-size: var(--t-xs); border-radius: var(--r-sm); border-bottom-width: 2px; }
.btn-sm:active { transform: translateY(1px); border-bottom-width: 1px; }
.btn-icon { padding: 0; width: 46px; }
.btn-sm.btn-icon { width: 36px; }
.btn-block { width: 100%; }
/* Small buttons grow a little on touch — 34px is too thin for a thumb, but
   a full 44px makes a toolbar fat. 40px is the middle. */
@media (pointer: coarse) {
  .btn-sm { min-height: 42px; padding-inline: var(--s4); }
  .btn-sm.btn-icon { width: 42px; padding-inline: 0; }
}

/* A button that is working: its label stays in place (so the button does
   not change width), only replaced by a spinning disc. */
.btn.is-loading { pointer-events: none; color: transparent; }
.btn.is-loading::after {
  content: ""; position: absolute; width: 16px; height: 16px;
  border: 2px solid color-mix(in srgb, var(--btn-fg) 30%, transparent);
  border-top-color: var(--btn-fg); border-radius: 50%;
  animation: spin 640ms linear infinite;
}
@keyframes spin { to { transform: rotate(360deg); } }
@media (prefers-reduced-motion: reduce) {
  .btn.is-loading::after { animation-duration: 2s; }
}

/* ── Surfaces ─────────────────────────────────────────────────────────────
   A card now has a surface of its own (--surface), lighter than the page
   canvas (--paper), plus one tight shadow. That is the order foto.hanif.app
   uses, and the reason is this: once a surface rises through LIGHT, its
   line may go back to a 1px hairline. The earlier version did the opposite
   — a surface the same colour as the page, separated by a 2px line — and
   the result read like a technical drawing, not like stacked objects. */
.card {
  background: var(--surface);
  border: var(--b) solid var(--line);
  border-radius: var(--r-card);
  padding: var(--s5);
  box-shadow: var(--shadow-s);
}
/* A flat card: one that should not rise, e.g. an explanatory block inside
   another card. It sinks IN instead, so its shadow is dropped. */
.card-flat { border-color: transparent; background: var(--sunk); box-shadow: none; }
.card-link {
  display: block; text-decoration: none;
  transition: transform var(--d-mid) var(--ease), box-shadow var(--d-mid) var(--ease),
              border-color var(--d-mid) var(--ease);
}
/* A card you can click rises ONE pixel and its shadow thickens. That number
   was chosen deliberately: enough for the eye to catch "this can be lifted",
   not enough to read as an animation. The earlier version jumped 3px with a
   full shadow (too busy, then stripped out entirely until there was no
   motion at all). The right answer sits between them — and this is exactly
   how the folder cards in foto.hanif.app behave. */
@media (hover: hover) {
  .card-link:hover {
    transform: translateY(-1px);
    box-shadow: var(--shadow-m);
    border-color: color-mix(in srgb, var(--accent) 34%, var(--line));
  }
}
.card-link:active { transform: translateY(0); box-shadow: var(--shadow-s); border-color: var(--accent-line); }
@media (prefers-reduced-motion: reduce) {
  .card-link, .card-link:hover, .card-link:active { transform: none; transition: none; }
}
.card-pad-0 { padding: 0; overflow: hidden; }
@media (max-width: 480px) { .card { padding: var(--s4); } }

/* Glass — bars and panels that sit over scrolling content. What the blur
   does here is not beauty: it stops the text passing behind from competing
   with the text on top, without having to cover it with an opaque field. */
.glass {
  background: color-mix(in srgb, var(--surface) 82%, transparent);
  backdrop-filter: saturate(140%) blur(var(--blur));
  -webkit-backdrop-filter: saturate(140%) blur(var(--blur));
  border: var(--b) solid var(--line);
  border-radius: var(--r-card);
  box-shadow: var(--shadow-s);
}

/* A full field of colour — for one emphatic block per page. One, not two:
   if there are two full colour fields on one screen, one of them is
   certainly not as important as it was thought to be. */
.slab {
  background: var(--accent); color: var(--on-accent);
  border-radius: var(--r-lg); padding: var(--s7);
}
.slab .muted, .slab .faint { color: color-mix(in srgb, currentColor 72%, transparent); }
/* Emphasis that does not spend the colour budget: solid ink. This is what
   you use when the accent is already taken elsewhere on the same screen. */
.slab-ink { background: var(--ink); color: var(--surface); }

/* The mat behind media. A little darker than the canvas so the edges of a
   light picture still read, and kept apart from --paper so it can be
   swapped on its own without changing the theme — like a mount on a desk. */
.mat { background: var(--mat); border-radius: var(--r-card); padding: var(--s3); }

/* Cover images: the ratio is held so the grid does not go ragged */
.thumb { aspect-ratio: 4 / 3; background: var(--mat); display: block; width: 100%; object-fit: cover; }
.thumb-wide { aspect-ratio: 16 / 9; }

/* A print — a picture with a margin around it, like a photograph printed
   with its white border left on. That margin is what makes the image read
   as an object that was placed, not as a texture stuck to the card. */
.plate {
  background: var(--surface);
  border: var(--b) solid var(--line);
  border-radius: var(--r-card);
  padding: var(--s2);
  box-shadow: var(--shadow-s);
  display: block;
}
.plate > img, .plate > .thumb { border-radius: calc(var(--r-card) - var(--s2)); display: block; width: 100%; }
.plate > figcaption {
  padding: var(--s2) var(--s1) 2px;
  font: 500 var(--t-meta)/1.4 var(--mono);
  letter-spacing: 0.06em; text-transform: uppercase; color: var(--ink-3);
}

/* ── Markers ──────────────────────────────────────────────────────────── */
.chip {
  display: inline-flex; align-items: center; gap: 6px;
  padding: 5px 11px; border-radius: var(--r-pill);
  background: var(--paper-2); color: var(--ink-2);
  font-size: var(--t-xs); font-weight: 560; white-space: nowrap;
}
.chip-solid { background: var(--accent); color: var(--on-accent); }
.chip-line { background: transparent; box-shadow: inset 0 0 0 1px var(--line-2); }
/* A chip you can press must be thumb sized — one that is only a label need not. */
a.chip, button.chip { min-height: 32px; cursor: pointer; border: 0; text-decoration: none; }
@media (pointer: coarse) { a.chip, button.chip { min-height: 40px; padding-inline: var(--s4); } }
.chip .x {
  margin-right: -4px; margin-left: 2px; width: 18px; height: 18px; flex: none;
  display: grid; place-items: center; border: 0; border-radius: 50%;
  background: color-mix(in srgb, currentColor 14%, transparent);
  color: inherit; font-size: 11px; line-height: 1; cursor: pointer;
}
.dot { width: 8px; height: 8px; border-radius: 50%; background: var(--accent); flex: none; }
.dot-green { background: var(--green); } .dot-red { background: var(--red); }
.dot-orange { background: var(--orange); } .dot-faint { background: var(--ink-3); }

.badge {
  display: inline-flex; align-items: center; padding: 2px 8px;
  border-radius: var(--r-sm); background: var(--accent-soft); color: var(--accent);
  font-size: var(--t-xs); font-weight: 640;
}

/* ── Inputs ───────────────────────────────────────────────────────────── */
.field { display: grid; gap: 6px; min-width: 0; }
.label { font-size: var(--t-sm); font-weight: 600; }
.input, .select, .textarea {
  width: 100%; min-height: 44px; padding: 10px var(--s3);
  background: var(--surface);
  /* The line is --line, not --line-2: since the system line thinned to a 1px
     hairline, the faintest one is now too faint to mark a box you can type
     in. An input must look like a box before it is touched, not after. */
  border: var(--b) solid var(--line); border-radius: var(--r-btn);
  /* 16px: below this iOS force-zooms when the box is tapped. */
  font-size: max(var(--t-md), 16px);
  transition: border-color var(--d-fast) var(--ease), box-shadow var(--d-fast) var(--ease);
}
.textarea { min-height: 104px; resize: vertical; padding-top: var(--s3); }
.input::placeholder, .textarea::placeholder { color: var(--ink-3); }
/* A soft 4px halo, not a thick ring. The real keyboard focus ring is still
   held by the global :focus-visible — this only marks which box is live. */
.input:focus, .select:focus, .textarea:focus {
  outline: 0; border-color: var(--accent);
  box-shadow: 0 0 0 4px color-mix(in srgb, var(--accent) 20%, transparent);
}
.input:disabled, .textarea:disabled, .select:disabled { background: var(--sunk); color: var(--ink-3); cursor: not-allowed; }
.input[aria-invalid="true"], .textarea[aria-invalid="true"] { border-color: var(--red); }
.hint { font-size: var(--t-xs); color: var(--ink-3); }
.hint-error { color: var(--red); font-weight: 560; }

.switch {
  appearance: none; width: 46px; height: 28px; border-radius: var(--r-pill);
  background: var(--line-2); position: relative; cursor: pointer; flex: none;
  transition: background var(--d-mid) var(--ease);
}
.switch::after {
  content: ""; position: absolute; inset: 3px auto 3px 3px; width: 22px;
  border-radius: 50%; background: var(--surface); box-shadow: var(--shadow-1);
  transition: transform var(--d-mid) var(--ease);
}
.switch:checked { background: var(--accent); }
.switch:checked::after { transform: translateX(18px); }

/* ── Tabs ─────────────────────────────────────────────────────────────────
   On a narrow screen the strip deliberately runs past the page edge, to make
   it clear the content can still be scrolled — not that it stops there. */
.tabs {
  display: flex; gap: var(--s1); border-bottom: 1px solid var(--line);
  overflow-x: auto; scrollbar-width: none; scroll-snap-type: x proximity;
}
.tabs::-webkit-scrollbar { display: none; }
.tab {
  border: 0; background: none; cursor: pointer; white-space: nowrap;
  padding: var(--s3); margin-bottom: -1px; scroll-snap-align: start;
  min-height: 44px;
  border-bottom: 2px solid transparent;
  color: var(--ink-2); font-size: var(--t-sm); font-weight: 600;
  transition: color var(--d-fast) var(--ease), border-color var(--d-fast) var(--ease);
}
@media (hover: hover) { .tab:hover { color: var(--ink); } }
.tab[aria-selected="true"] { color: var(--ink); border-bottom-color: var(--accent); }
@media (max-width: 640px) {
  .tabs { margin-inline: calc(var(--gutter) * -1); padding-inline: var(--gutter); }
}

/* ── Tables ───────────────────────────────────────────────────────────────
   Tables on a phone are a real problem: 480px of minimum content will never
   fit in 375px. The system offers two ways out, chosen deliberately:
   .tablewrap   → stays a table, scrolls sideways. For data that really must
                  be compared across columns (reports, figures).
   .table-stack → each row becomes a card, the column name moves to the left
                  of each value. For lists read one at a time (orders, users).
                  Needs data-label on every <td>. */
.tablewrap {
  overflow-x: auto; border: 1px solid var(--line); border-radius: var(--r-card);
  -webkit-overflow-scrolling: touch;
}
.table { width: 100%; border-collapse: collapse; font-size: var(--t-sm); min-width: 480px; }
.table th {
  text-align: left; padding: var(--s3) var(--s4);
  font-size: var(--t-xs); font-weight: 640; letter-spacing: 0.06em; text-transform: uppercase;
  color: var(--ink-3); background: var(--paper-2); white-space: nowrap;
}
.table td { padding: var(--s3) var(--s4); border-top: 1px solid var(--line); vertical-align: middle; }
@media (hover: hover) { .table tbody tr:hover { background: var(--paper-2); } }
.table .num { text-align: right; font-family: var(--mono); font-variant-numeric: tabular-nums; white-space: nowrap; }
.table-selectable tbody tr[aria-selected="true"] { background: var(--accent-soft); }

/* A table head that sticks while scrolling — must be paired with a wrapper
   that has a fixed height, e.g. .tablewrap style="max-height:420px" */
.table-sticky thead th { position: sticky; top: 0; z-index: 2; }
/* The first column sticks while scrolling sideways — the row's name does not
   disappear while someone is looking at the rightmost column. */
.table-pin th:first-child, .table-pin td:first-child {
  position: sticky; left: 0; z-index: 1; background: var(--surface);
  box-shadow: 1px 0 0 var(--line);
}
.table-pin thead th:first-child { z-index: 3; background: var(--paper-2); }

/* Sortable column heads */
.table th.sortable { cursor: pointer; user-select: none; }
.table th.sortable::after { content: "↕"; margin-left: 6px; opacity: .35; font-size: 11px; }
.table th[aria-sort="ascending"]::after { content: "↑"; opacity: 1; color: var(--accent); }
.table th[aria-sort="descending"]::after { content: "↓"; opacity: 1; color: var(--accent); }

@media (max-width: 640px) {
  .table-stack { min-width: 0; }
  .table-stack thead { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); }
  .table-stack tbody tr {
    display: grid; gap: 2px;
    padding: var(--s3) var(--s4);
    border-top: 1px solid var(--line);
  }
  .table-stack tbody tr:first-child { border-top: 0; }
  .table-stack td { display: grid; grid-template-columns: 40% 1fr; gap: var(--s3); padding: 5px 0; border: 0; }
  .table-stack td::before {
    content: attr(data-label);
    font-size: var(--t-xs); font-weight: 640; letter-spacing: .06em;
    text-transform: uppercase; color: var(--ink-3);
  }
  .table-stack td.num { text-align: left; }
  /* The first cell becomes the card's heading */
  .table-stack td:first-child { grid-template-columns: 1fr; font-weight: 620; font-size: var(--t-md); }
  .table-stack td:first-child::before { display: none; }
}

/* ── Big numbers ──────────────────────────────────────────────────────── */
.stat { display: grid; gap: 2px; min-width: 0; }
.stat dt { font-size: var(--t-xs); font-weight: 620; letter-spacing: 0.06em; text-transform: uppercase; color: var(--ink-3); }
.stat dd { font-size: var(--t-3xl); font-weight: 660; letter-spacing: -0.03em; line-height: 1; font-variant-numeric: tabular-nums; }
.stat dd small { font-size: var(--t-sm); font-weight: 500; color: var(--ink-2); letter-spacing: 0; margin-left: 6px; }

/* The up/down difference. Its colour has two directions only — do not add more. */
.trend { display: inline-flex; align-items: center; gap: 5px; font-size: var(--t-xs); font-weight: 640; font-variant-numeric: tabular-nums; }
.trend .ico { width: 12px; height: 12px; vertical-align: 0; }
.trend-up { color: var(--grass); }
.trend-down { color: var(--tomato); }
.trend-flat { color: var(--ink-3); }

/* A full figure card: label, number, difference, and a small chart beneath. */
.statcard { display: grid; gap: var(--s3); min-width: 0; }
.statcard .statcard-top { display: flex; align-items: baseline; justify-content: space-between; gap: var(--s3); flex-wrap: wrap; }

/* ── Top bar ──────────────────────────────────────────────────────────────
   Sticks to the top and respects the device notch. Its height comes from a
   token so anchors (:target) and sticky sidebars know how far to drop. */
.topbar {
  position: sticky; top: 0; z-index: var(--z-topbar);
  display: flex; align-items: center; gap: var(--s4);
  height: calc(var(--h-topbar) + var(--safe-t));
  padding-top: var(--safe-t);
  background: color-mix(in srgb, var(--paper) 86%, transparent);
  backdrop-filter: saturate(180%) blur(14px);
  border-bottom: 1px solid var(--line);
  /* Content that overflows scrolls rather than pushing the page wider. Which
     items may disappear on a narrow screen is decided by the page itself
     (with .hide-sm), not here. */
  overflow-x: clip;
}
.brand { display: inline-flex; align-items: center; gap: var(--s2); font-weight: 680; letter-spacing: -0.02em; text-decoration: none; white-space: nowrap; }
.brand-mark { width: 26px; height: 26px; border-radius: 8px; background: var(--ink); display: grid; place-items: center; color: var(--surface); font-size: 13px; font-weight: 700; flex: none; }
.navlink { color: var(--ink-2); text-decoration: none; font-size: var(--t-sm); font-weight: 560; padding: 8px 10px; border-radius: var(--r-sm); white-space: nowrap; }
@media (hover: hover) { .navlink:hover { color: var(--ink); background: var(--paper-2); } }
.navlink[aria-current="page"] { color: var(--ink); background: var(--paper-2); }

/* Drawer button — only on narrow screens, three plain lines. */
.burger {
  display: none; width: 44px; height: 44px; flex: none;
  align-items: center; justify-content: center;
  border: 0; border-radius: var(--r-sm); background: transparent; cursor: pointer; color: var(--ink);
}
.burger i { display: block; width: 20px; height: var(--b); background: currentColor; border-radius: 2px; position: relative; }
.burger i::before, .burger i::after { content: ""; position: absolute; left: 0; width: 20px; height: var(--b); background: currentColor; border-radius: 2px; }
.burger i::before { top: -7px; } .burger i::after { top: 7px; }
@media (max-width: 900px) { .burger { display: inline-flex; } }

/* ── Sidebar ──────────────────────────────────────────────────────────────
   Sticks below the top bar and SCROLLS ON ITS OWN. That last part is not a
   decoration: the table of contents on this documentation page is 3276px
   tall on a 996px screen, and without max-height + overflow two thirds of
   its links sit off screen permanently — not scrollable, not clickable,
   with no hint at all that they exist. A sticky element does not move with
   the page, so anything past the screen height is simply gone.

   The shape: fixed head (search box) + scrolling list + fixed foot.
   min-height:0 on the flex child is required — without it the list refuses
   to shrink and its overflow never turns on. */
.side {
  position: sticky;
  top: calc(var(--h-topbar) + var(--s5));
  align-self: start;
  display: flex;
  flex-direction: column;
  min-width: 0;
  max-height: calc(100dvh - var(--h-topbar) - var(--s6));
  gap: var(--s2);
}
.side-list {
  flex: 1 1 auto;
  min-height: 0;
  overflow-y: auto;
  overscroll-behavior: contain;
  display: grid;
  gap: 1px;
  align-content: start;
  /* room for the scrollbar so the text does not touch the wall */
  padding-right: 4px;
  margin-right: -4px;
  scrollbar-width: thin;
  scrollbar-color: var(--line-2) transparent;
}
.side-list::-webkit-scrollbar { width: 8px; }
.side-list::-webkit-scrollbar-track { background: transparent; }
.side-list::-webkit-scrollbar-thumb { background: var(--line-2); border-radius: 99px; border: 2px solid var(--paper); }

/* Search box — the matching engine is fuzzy, see lib/fuzzy.js */
.side-search {
  display: flex; align-items: center; gap: var(--s2); flex: none;
  padding: 0 var(--s3);
  border: var(--b) solid var(--line-2); border-radius: var(--r-btn);
  background: var(--surface);
  transition: border-color var(--d-fast) var(--ease), box-shadow var(--d-fast) var(--ease);
}
.side-search:focus-within { border-color: var(--accent); box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent) 22%, transparent); }
.side-search .ico { color: var(--ink-3); }
.side-search input {
  flex: 1; min-width: 0; border: 0; outline: 0; background: none;
  padding: 9px 0; font-size: max(var(--t-sm), 16px);
}
.side-search input::-webkit-search-cancel-button { display: none; }
.side-search .kbd { flex: none; }
.side-search:focus-within .kbd { display: none; }

/* Groups that fold */
.side-group > summary {
  display: flex; align-items: center; gap: var(--s2);
  padding: 7px 10px; cursor: pointer; list-style: none;
  font-size: var(--t-xs); font-weight: 660; letter-spacing: .08em;
  text-transform: uppercase; color: var(--ink-3);
  border-radius: var(--r-sm);
  user-select: none;
}
.side-group > summary::-webkit-details-marker { display: none; }
.side-group > summary::before {
  content: ""; flex: none; width: 0; height: 0;
  border-left: 4px solid currentColor;
  border-block: 3.5px solid transparent;
  transition: transform var(--d-fast) var(--ease);
}
.side-group[open] > summary::before { transform: rotate(90deg); }
@media (hover: hover) { .side-group > summary:hover { background: var(--paper-2); color: var(--ink-2); } }
.side-group > summary .n {
  margin-left: auto; font-family: var(--mono); font-weight: 600;
  letter-spacing: 0; color: var(--ink-3);
}
/* The group being read is marked, so it stays visible even while it is
   folded */
.side-group.is-active > summary { color: var(--ink); }
.side-group.is-active > summary .n { color: var(--accent); }
.side-items { display: grid; gap: 1px; padding-bottom: var(--s2); }

.side-none { padding: var(--s5) var(--s3); font-size: var(--t-sm); color: var(--ink-2); line-height: 1.6; }
.side-foot { flex: none; display: flex; gap: var(--s4); padding: var(--s2) 10px 0; border-top: 1px solid var(--line); font-size: var(--t-xs); }
.side-foot .kbd { margin-right: 3px; }
.side mark { background: color-mix(in srgb, var(--accent) 18%, transparent); color: inherit; border-radius: 3px; padding: 0 1px; }
.side a {
  display: block; padding: 8px 10px; border-radius: var(--r-sm);
  color: var(--ink-2); text-decoration: none; font-size: var(--t-sm);
  scroll-margin-block: var(--s5);
}
/* The keyboard highlight is deliberately different from the "being read"
   marker: one answers what a person typed, the other reports position. Make
   them the same and both are lost. */
.side a.is-marked { background: var(--accent-soft); color: var(--accent); }
@media (hover: hover) { .side a:hover { background: var(--paper-2); color: var(--ink); } }
.side a[aria-current="true"], .side a[aria-current="page"] { background: var(--ink); color: var(--surface); font-weight: 600; }
.side .group { margin-top: var(--s4); padding: 0 10px; font-size: var(--t-xs); font-weight: 660; letter-spacing: 0.08em; text-transform: uppercase; color: var(--ink-3); }
@media (pointer: coarse) { .side a { padding-block: 11px; } }

/* ── Bottom bar (phones) ──────────────────────────────────────────────────
   For apps whose content is a few equal sections. Only on narrow screens —
   on a laptop its place is the sidebar or the top bar, not stuck to the
   bottom of the screen. */
.bottomnav {
  position: fixed; inset: auto 0 0 0; z-index: var(--z-topbar);
  display: none; justify-content: space-around; align-items: stretch;
  padding-bottom: var(--safe-b);
  background: color-mix(in srgb, var(--paper) 92%, transparent);
  backdrop-filter: saturate(180%) blur(14px);
  border-top: 1px solid var(--line);
}
.bottomnav a {
  flex: 1; min-height: 52px; display: grid; place-items: center; gap: 2px;
  padding: var(--s2) var(--s1); text-decoration: none;
  color: var(--ink-3); font-size: 11px; font-weight: 600;
}
.bottomnav a span { font-size: 19px; line-height: 1; }
.bottomnav a[aria-current="page"] { color: var(--accent); }
@media (max-width: 640px) {
  .bottomnav { display: flex; }
  /* Room so the last of the page is not covered by the bar */
  .has-bottomnav { padding-bottom: calc(64px + var(--safe-b)); }
}

/* ── The rest ─────────────────────────────────────────────────────────── */
.rule { height: 1px; background: var(--line); border: 0; }
.empty { display: grid; gap: var(--s3); justify-items: center; text-align: center; padding: var(--s8) var(--s4); }
.empty .empty-art { color: var(--ink-3); }
.progress { height: 6px; border-radius: 3px; background: var(--paper-3); overflow: hidden; }
.progress i { display: block; height: 100%; background: var(--accent); border-radius: 3px; transition: width var(--d-mid) var(--ease); }
.avatar { width: 36px; height: 36px; border-radius: 50%; background: var(--paper-3); display: grid; place-items: center; font-size: var(--t-xs); font-weight: 640; flex: none; overflow: hidden; }
.avatar img { width: 100%; height: 100%; object-fit: cover; }
.avatar-sm { width: 26px; height: 26px; font-size: 10px; }
.avatar-lg { width: 52px; height: 52px; font-size: var(--t-sm); }

.code {
  font: 0.8125rem/1.75 var(--mono);
  background: var(--paper-2); border: var(--b) solid var(--line); border-radius: var(--r-card);
  padding: var(--s4); overflow-x: auto; white-space: pre; color: var(--ink);
  -webkit-overflow-scrolling: touch;
  tab-size: 2;
}

/* ── Code colour ──────────────────────────────────────────────────────────
   TWO colours, not six. This is not thrift, it is the same system rule that
   governs the whole page: colour is only a signal, everything else is
   disciplined monochrome. A six-colour highlighter looks clever on a single
   block, but one documentation page can hold seventy blocks — and there the
   colour stops being a signal and turns into noise that changes the feel of
   the entire page.

   Only two things are told apart, because only two are what the eye hunts
   for when scanning code: where the STRUCTURE is (tag names, keywords,
   selectors) and where the CONTENT is (text inside quotes). The rest is
   plain ink, with comments dimmed. Mustard is never used here — on paper
   its contrast is only 2.9:1, too thin for letters this small. */
.code i, .prose pre i { font-style: normal; }
.t-com   { color: var(--ink-3); font-style: italic !important; }
.t-str   { color: var(--tomato); }
.t-tag,
.t-key,
.t-sel   { color: var(--denim); font-weight: 620; }
/* Deliberately plain ink: attributes, numbers, function names and CSS
   properties follow the reading flow instead of each asking for attention. */
.t-attr,
.t-num,
.t-fn,
.t-prop   { color: var(--ink); }
.t-punc  { color: var(--ink-3); }

/* ── When someone asks for less motion ───────────────────────────────────
   This is not simply "turn everything off". Shortening a duration is NOT
   stopping: for an `infinite` animation, a 1ms duration means it repeats
   thousands of times a second — it flickers, which is precisely the worst
   thing for someone sensitive to motion. The exact opposite of what was
   asked for.

   So the order is: anything that repeats forever (a pulsing skeleton, a
   spinner, typing dots) is TURNED OFF COMPLETELY and replaced with a still
   state that still reads. Everything else — the one-shot animations — has
   its duration cut and its iteration count locked to one, so that if some
   new `infinite` animation slips in later, it still cannot flicker.

   The earlier version wrote `animation-duration: 1ms !important` here, and
   that quietly beat `.btn.is-loading::after { animation-duration: 2s }`
   above it — the intent was to slow the spinner down, the result was a
   spinner that flickered. */
@media (prefers-reduced-motion: reduce) {
  .skeleton,
  .saved.is-saving::before,
  .chat .typing i,
  .btn.is-loading::after { animation: none !important; }

  /* What used to pulse must still read as "waiting" — so its still state is
     taken from the dimmest point of its animation. */
  .skeleton { opacity: .78; }
  .chat .typing i { opacity: .55; }
  /* The button spinner stops turning; so it does not read as a broken ring,
     it becomes a complete circle, dimmed. */
  .btn.is-loading::after { border-top-color: color-mix(in srgb, var(--btn-fg) 30%, transparent); opacity: .7; }

  *, *::before, *::after {
    animation-duration: 1ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 1ms !important;
  }
}

/* ═══════════════════════════════════════════════════════════════════════
   Larger components
   Wherever possible these ride on built-in HTML elements — <details>,
   <dialog>, popover, <input type="range"> — so their behaviour is correct
   without JavaScript and they stay reachable from the keyboard.
   ═══════════════════════════════════════════════════════════════════════ */

/* ── Notices ──────────────────────────────────────────────────────────── */
.alert {
  display: grid; grid-template-columns: auto minmax(0, 1fr); gap: var(--s3);
  padding: var(--s4); border-radius: var(--r-card);
  background: var(--paper-2); border: 1px solid var(--line);
}
.alert-icon { width: 22px; height: 22px; display: grid; place-items: center; color: var(--ink-3); }
.alert-icon .ico { width: 22px; height: 22px; vertical-align: 0; }
.alert strong { display: block; font-weight: 640; margin-bottom: 2px; }
.alert p { color: var(--ink-2); font-size: var(--t-sm); }
.alert-accent { background: var(--accent-soft); border-color: var(--accent-line); }
.alert-accent .alert-icon, .alert-accent strong { color: var(--accent); }
.alert-danger { --accent: var(--red); --on-accent: var(--on-red); }
.alert-ok { --accent: var(--green); --on-accent: var(--on-green); }
.alert-warn { --accent: var(--orange); --on-accent: var(--on-orange); }

/* A page-wide announcement bar — one line, dismissible. */
.banner {
  display: flex; align-items: center; gap: var(--s3); flex-wrap: wrap;
  padding: var(--s3) var(--gutter);
  padding-top: calc(var(--s3) + var(--safe-t));
  background: var(--ink); color: var(--surface);
  font-size: var(--t-sm); font-weight: 560;
}
.banner .btn { margin-left: auto; }
.banner-accent { background: var(--accent); color: var(--on-accent); }

/* ── Breadcrumbs & paging ─────────────────────────────────────────────── */
.crumbs { display: flex; align-items: center; gap: var(--s2); font-size: var(--t-sm); flex-wrap: wrap; }
.crumbs a { color: var(--ink-2); text-decoration: none; }
.crumbs a:hover { color: var(--ink); text-decoration: underline; }
.crumbs [aria-current] { color: var(--ink); font-weight: 600; }
.crumbs .sep { color: var(--ink-3); }

.pager { display: flex; gap: var(--s1); align-items: center; flex-wrap: wrap; }
.pager button, .pager a {
  min-width: 38px; height: 38px; padding: 0 10px;
  display: inline-grid; place-items: center;
  border: 0; border-radius: var(--r-sm); background: transparent; color: var(--ink-2);
  font-size: var(--t-sm); font-weight: 600; cursor: pointer; text-decoration: none;
}
@media (hover: hover) { .pager button:hover, .pager a:hover { background: var(--paper-2); color: var(--ink); } }
.pager [aria-current="page"] { background: var(--ink); color: var(--surface); }
.pager button:disabled { opacity: .35; pointer-events: none; }
@media (pointer: coarse) { .pager button, .pager a { min-width: 44px; height: 44px; } }

/* ── Choices ──────────────────────────────────────────────────────────── */
.check, .radio {
  appearance: none; width: 20px; height: 20px; flex: none;
  border: var(--b) solid var(--line-2); background: var(--surface);
  display: inline-grid; place-items: center; cursor: pointer;
  transition: background var(--d-fast) var(--ease), border-color var(--d-fast) var(--ease);
}
.check { border-radius: 6px; }
.radio { border-radius: 50%; }
.check:checked, .radio:checked { background: var(--accent); border-color: var(--accent); }
.check:checked::after {
  content: ""; width: 10px; height: 6px; margin-top: -2px;
  border-left: 2px solid var(--on-accent); border-bottom: 2px solid var(--on-accent);
  transform: rotate(-45deg);
}
.radio:checked::after { content: ""; width: 8px; height: 8px; border-radius: 50%; background: var(--on-accent); }
/* The label is the touch target, not the 20px box. */
.choice { display: inline-flex; align-items: center; gap: var(--s2); cursor: pointer; font-size: var(--t-sm); min-height: 44px; }

/* Two-state button group */
.toggles { display: inline-flex; padding: 3px; gap: 2px; background: var(--paper-2); border-radius: var(--r-btn); max-width: 100%; overflow-x: auto; scrollbar-width: none; }
.toggles::-webkit-scrollbar { display: none; }
.toggles button {
  border: 0; background: transparent; cursor: pointer; color: var(--ink-2);
  padding: 7px 14px; border-radius: 9px; font-size: var(--t-sm); font-weight: 600; white-space: nowrap;
  transition: background var(--d-fast) var(--ease), color var(--d-fast) var(--ease);
}
@media (hover: hover) { .toggles button:hover { color: var(--ink); } }
.toggles button[aria-pressed="true"] { background: var(--surface); color: var(--ink); box-shadow: var(--shadow-1); }
/* Full-width variant — good for two or three choices on a phone */
.toggles-block { display: flex; width: 100%; }
.toggles-block button { flex: 1; }
@media (pointer: coarse) { .toggles button { min-height: 38px; } }

/* Value slider */
.slider {
  appearance: none; width: 100%; height: 6px; border-radius: 3px;
  background: var(--paper-3); cursor: pointer;
}
.slider::-webkit-slider-thumb {
  appearance: none; width: 22px; height: 22px; border-radius: 50%;
  background: var(--surface); border: 2px solid var(--accent); box-shadow: var(--shadow-1);
}
.slider::-moz-range-thumb { width: 22px; height: 22px; border: 2px solid var(--accent); border-radius: 50%; background: var(--surface); }
@media (pointer: coarse) {
  .slider { height: 8px; }
  .slider::-webkit-slider-thumb { width: 28px; height: 28px; }
  .slider::-moz-range-thumb { width: 28px; height: 28px; }
}

/* Joined input: prefix and suffix sit in one field */
.inputgroup { display: flex; align-items: stretch; min-width: 0; }
.inputgroup > .input { border-radius: 0; margin-left: calc(var(--b) * -1); min-width: 0; }
.inputgroup > :first-child { border-start-start-radius: var(--r-btn); border-end-start-radius: var(--r-btn); margin-left: 0; }
.inputgroup > :last-child { border-start-end-radius: var(--r-btn); border-end-end-radius: var(--r-btn); }
.inputgroup .affix {
  display: grid; place-items: center; padding: 0 var(--s3);
  background: var(--paper-2); border: var(--b) solid var(--line-2);
  color: var(--ink-2); font-size: var(--t-sm); white-space: nowrap;
}

/* One-time code — the boxes shrink themselves so six digits still fit on a
   320px screen without anything being cut off. */
.otp { display: flex; gap: var(--s2); max-width: 100%; }
.otp input {
  flex: 1 1 0; min-width: 0; max-width: 52px;
  height: 54px; text-align: center; font: 620 var(--t-lg)/1 var(--mono);
  border: var(--b) solid var(--line-2); border-radius: var(--r-btn); background: var(--surface);
}
.otp input:focus { outline: 0; border-color: var(--accent); box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent) 22%, transparent); }

/* Number box with minus/plus buttons */
.stepper { display: inline-flex; align-items: center; border: var(--b) solid var(--line-2); border-radius: var(--r-btn); overflow: hidden; }
.stepper button {
  width: 42px; height: 42px; flex: none; border: 0; background: var(--paper-2);
  color: var(--ink); font-size: var(--t-lg); line-height: 1; cursor: pointer;
}
@media (hover: hover) { .stepper button:hover { background: var(--paper-3); } }
.stepper button:disabled { opacity: .35; pointer-events: none; }
.stepper input {
  width: 56px; height: 42px; border: 0; text-align: center; background: var(--surface);
  font: 620 var(--t-md)/1 var(--mono); font-variant-numeric: tabular-nums;
  appearance: textfield;
}
.stepper input::-webkit-outer-spin-button, .stepper input::-webkit-inner-spin-button { appearance: none; margin: 0; }
.stepper input:focus { outline: 0; }

/* Star rating — real radios, so it works from the keyboard */
.rating { display: inline-flex; flex-direction: row-reverse; gap: 2px; }
.rating input { position: absolute; opacity: 0; width: 0; height: 0; }
.rating label {
  cursor: pointer; font-size: 26px; line-height: 1; color: var(--line-2);
  padding: 4px; transition: color var(--d-fast) var(--ease);
}
.rating label::before { content: "★"; }
.rating input:checked ~ label, .rating label:hover, .rating label:hover ~ label { color: var(--yellow); }
.rating input:focus-visible + label { outline: 3px solid var(--accent); outline-offset: 2px; border-radius: 4px; }

/* Chips inside an input */
.taginput {
  display: flex; flex-wrap: wrap; align-items: center; gap: 6px;
  min-height: 44px; padding: 6px var(--s2);
  background: var(--surface); border: var(--b) solid var(--line-2); border-radius: var(--r-btn);
}
.taginput:focus-within { border-color: var(--accent); box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent) 22%, transparent); }
.taginput input {
  flex: 1 1 90px; min-width: 90px; border: 0; outline: 0; background: none;
  font-size: max(var(--t-sm), 16px); padding: 4px 2px;
}

/* Drop area for files */
.dropzone {
  display: grid; place-items: center; gap: var(--s2); text-align: center;
  padding: var(--s7) var(--s4); cursor: pointer;
  border: var(--b) dashed var(--line-2); border-radius: var(--r-card);
  background: var(--paper-2); color: var(--ink-2);
  transition: border-color var(--d-fast) var(--ease), background var(--d-fast) var(--ease);
}
.dropzone:focus-within, .dropzone.is-over { border-color: var(--accent); background: var(--accent-soft); color: var(--accent); }
.dropzone input[type="file"] { position: absolute; width: 1px; height: 1px; opacity: 0; }
.dropzone .dz-art { color: var(--ink-3); line-height: 1; }

/* Edit in place: plain text that becomes an input when clicked */
.inline-edit {
  display: inline-flex; align-items: center; gap: var(--s2); min-height: 34px;
  padding: 2px 8px; margin-inline: -8px; border-radius: var(--r-sm);
  border: var(--b) solid transparent; background: none; cursor: text;
  font: inherit; color: inherit; text-align: left; width: 100%;
}
@media (hover: hover) { .inline-edit:hover { background: var(--paper-2); } }
.inline-edit:focus { outline: 0; border-color: var(--accent); background: var(--surface); cursor: text; }
.inline-edit::after { content: "✎"; margin-left: auto; color: var(--ink-3); font-size: var(--t-xs); opacity: 0; }
@media (hover: hover) { .inline-edit:hover::after { opacity: 1; } }

/* ── Folds ────────────────────────────────────────────────────────────── */
.accordion { border: 1px solid var(--line); border-radius: var(--r-card); overflow: hidden; }
.accordion > details + details { border-top: 1px solid var(--line); }
.accordion summary, .disclosure summary {
  cursor: pointer; padding: var(--s4); font-weight: 600; list-style: none;
  display: flex; align-items: center; gap: var(--s3); min-height: 44px;
}
.accordion summary::-webkit-details-marker, .disclosure summary::-webkit-details-marker { display: none; }
/* The marker is drawn from two lines, not the character "＋", whose shape
   differs in every font and is never truly centred. */
.accordion summary::after, .disclosure summary::after {
  content: ""; margin-left: auto; flex: none; width: 13px; height: 13px;
  background: currentColor; color: var(--ink-3);
  clip-path: polygon(0 45%, 45% 45%, 45% 0, 55% 0, 55% 45%, 100% 45%, 100% 55%, 55% 55%, 55% 100%, 45% 100%, 45% 55%, 0 55%);
  transition: transform var(--d-mid) var(--ease), clip-path var(--d-mid) var(--ease);
}
.accordion details[open] summary::after, .disclosure[open] summary::after {
  clip-path: polygon(0 45%, 100% 45%, 100% 55%, 0 55%);
}
.accordion .body, .disclosure .body { padding: 0 var(--s4) var(--s4); color: var(--ink-2); font-size: var(--t-sm); }
@media (hover: hover) { .accordion summary:hover { background: var(--paper-2); } }

/* ── Layers: menu, popover, tooltip ───────────────────────────────────── */
.menu {
  min-width: 200px; max-width: min(320px, calc(100vw - 24px));
  padding: 6px; border-radius: var(--r-card);
  background: var(--surface); border: 1px solid var(--line); box-shadow: var(--shadow-2);
}
.menu button, .menu a {
  display: flex; align-items: center; gap: var(--s3); width: 100%;
  padding: 9px 10px; border: 0; border-radius: var(--r-sm); background: none;
  color: var(--ink); font-size: var(--t-sm); text-align: left; cursor: pointer; text-decoration: none;
}
@media (hover: hover) { .menu button:hover, .menu a:hover { background: var(--paper-2); } }
.menu button:focus-visible, .menu a:focus-visible { background: var(--paper-2); }
.menu .danger { color: var(--red); }
.menu .shortcut { margin-left: auto; color: var(--ink-3); font: var(--t-xs)/1 var(--mono); }
.menu hr { height: 1px; background: var(--line); border: 0; margin: 5px 0; }
.menu .head { padding: 6px 10px; font-size: var(--t-xs); font-weight: 640; letter-spacing: .06em; text-transform: uppercase; color: var(--ink-3); }
@media (pointer: coarse) { .menu button, .menu a { min-height: 44px; } }

/* The browser's own popover: shows in the top layer with no script */
[popover] { border: 0; padding: 0; background: transparent; overflow: visible; }
[popover]::backdrop { background: transparent; }
.pop { animation: pop-in 140ms var(--ease); }
@keyframes pop-in { from { opacity: 0; transform: translateY(-4px) scale(.98); } to { opacity: 1; transform: none; } }

/* Short hints. The width is capped and the text may wrap, so a long
   sentence does not run off the screen. If the trigger is near an edge, use
   .tip-start / .tip-end to align the box to the safe side. */
.tip { position: relative; display: inline-flex; }
.tip > .tiptext {
  position: absolute; bottom: calc(100% + 8px); left: 50%; transform: translate(-50%, 4px);
  z-index: var(--z-pop);
  background: var(--ink); color: var(--surface); padding: 6px 10px; border-radius: var(--r-sm);
  font-size: var(--t-xs); font-weight: 560; line-height: 1.4; text-align: center;
  width: max-content; max-width: min(260px, calc(100vw - 32px));
  pointer-events: none;
  opacity: 0; transition: opacity var(--d-fast) var(--ease), transform var(--d-fast) var(--ease);
}
.tip-start > .tiptext { left: 0; transform: translate(0, 4px); text-align: left; }
.tip-end > .tiptext { left: auto; right: 0; transform: translate(0, 4px); text-align: left; }
.tip:hover > .tiptext, .tip:focus-within > .tiptext { opacity: 1; transform: translate(-50%, 0); }
.tip-start:hover > .tiptext, .tip-start:focus-within > .tiptext,
.tip-end:hover > .tiptext, .tip-end:focus-within > .tiptext { transform: translate(0, 0); }

/* On a narrow screen the box aligns to its trigger rather than centring on
   it. Centring means half the box hangs to the left of the trigger — and if
   the trigger is already near the edge, that half goes off screen. Aligning
   left removes the whole problem at once. */
@media (max-width: 480px) {
  .tip > .tiptext { left: 0; transform: translate(0, 4px); text-align: left; }
  .tip:hover > .tiptext, .tip:focus-within > .tiptext { transform: translate(0, 0); }
  .tip-end > .tiptext { left: auto; right: 0; }
}

/* A hover card. On a narrow screen it detaches from its trigger and pins to
   both screen edges — hung to the left of the trigger, its content is cut. */
.hovercard { position: relative; display: inline-flex; }
.hovercard > .hovercard-panel {
  position: absolute; top: calc(100% + 8px); left: 0; z-index: var(--z-pop);
  width: 260px; max-width: calc(100vw - 32px);
  padding: var(--s4); border-radius: var(--r-card); background: var(--surface);
  border: 1px solid var(--line); box-shadow: var(--shadow-2);
  opacity: 0; visibility: hidden; transform: translateY(4px);
  transition: opacity var(--d-fast) var(--ease), transform var(--d-fast) var(--ease), visibility var(--d-fast);
}
.hovercard:hover > .hovercard-panel, .hovercard:focus-within > .hovercard-panel { opacity: 1; visibility: visible; transform: none; }
@media (max-width: 640px) {
  .hovercard > .hovercard-panel { position: fixed; left: var(--s4); right: var(--s4); top: auto; width: auto; max-width: none; }
}

/* ── Dialog ───────────────────────────────────────────────────────────────
   A centred box on a laptop. On a phone it drops into a sheet rising from
   the bottom of the screen — the thumb is down there, and that is the shape
   people already know from native apps. */
.dialog {
  border: 0; padding: 0; border-radius: var(--r-lg); background: var(--surface); color: var(--ink);
  width: min(440px, calc(100vw - 32px)); max-height: calc(100dvh - 48px); overflow: auto;
  box-shadow: var(--shadow-2);
  /* Centred. The browser centres a modal dialog through `margin: auto` in
     its own stylesheet — and the global reset at the top of this file wipes
     that margin, so without this line every dialog sits in the top-left
     corner of the screen. */
  margin: auto;
}
.dialog::backdrop { background: rgb(0 0 0 / 0.45); backdrop-filter: blur(2px); }
.dialog-body { padding: var(--s5); display: grid; gap: var(--s3); }
.dialog-foot { padding: 0 var(--s5) var(--s5); display: flex; gap: var(--s2); justify-content: flex-end; }
.dialog[open] { animation: dialog-in var(--d-mid) var(--ease); }
@keyframes dialog-in { from { opacity: 0; transform: scale(.97); } to { opacity: 1; transform: none; } }

@media (max-width: 640px) {
  .dialog {
    width: 100vw; max-width: none; margin: auto auto 0;
    border-radius: var(--r-lg) var(--r-lg) 0 0;
    max-height: 90dvh;
    padding-bottom: var(--safe-b);
  }
  .dialog[open] { animation: dialog-up var(--d-mid) var(--ease); }
  @keyframes dialog-up { from { transform: translateY(100%); } to { transform: none; } }
  /* The buttons become equal width, easy to hit with a thumb */
  .dialog-foot { flex-direction: column-reverse; }
  .dialog-foot > .btn { width: 100%; }
  /* A small bar on top, the sign that this can be swiped down */
  .dialog::before {
    content: ""; display: block; width: 38px; height: 4px; border-radius: 2px;
    background: var(--line-2); margin: 10px auto 0;
  }
}

/* ── Sliding panels (sheet / drawer) ──────────────────────────────────── */
.sheet {
  border: 0; padding: 0; margin: 0; max-width: none; max-height: none;
  width: min(420px, 100vw); height: 100dvh;
  background: var(--surface); color: var(--ink); box-shadow: var(--shadow-2);
  margin-left: auto; /* pinned to the right */
}
.sheet::backdrop { background: rgb(0 0 0 / .45); }
.sheet[open] { animation: sheet-in var(--d-mid) var(--ease); }
@keyframes sheet-in { from { transform: translateX(24px); opacity: .6; } to { transform: none; opacity: 1; } }
.sheet-body {
  padding: var(--s5); display: grid; gap: var(--s4); align-content: start;
  height: 100%; overflow-y: auto; overscroll-behavior: contain;
  padding-top: max(var(--s5), var(--safe-t));
  padding-bottom: max(var(--s5), var(--safe-b));
}
.sheet-bottom {
  width: 100vw; height: auto; max-height: 86dvh; margin: auto auto 0;
  border-radius: var(--r-lg) var(--r-lg) 0 0;
}
/* The swipe bar on a bottom sheet — the sign that this can be pulled down */
.sheet-bottom::before {
  content: ""; display: block; width: 38px; height: 4px; border-radius: 2px;
  background: var(--line-2); margin: 10px auto 0;
}
.sheet-bottom[open] { animation: sheet-up var(--d-mid) var(--ease); }
@keyframes sheet-up { from { transform: translateY(100%); } to { transform: none; } }
@media (max-width: 640px) { .sheet { width: 100vw; } }

/* ── Command palette ──────────────────────────────────────────────────────
   On a phone it sits near the top: the keyboard covers the lower half of
   the screen, so the results list needs its room above. */
.command {
  border: 0; padding: 0; width: min(560px, calc(100vw - 32px));
  border-radius: var(--r-lg); background: var(--surface); box-shadow: var(--shadow-2);
  /* Near the top, centred sideways. `auto` on the sides is required for the
     same reason as .dialog: the global reset removed the browser's own
     centring margin. */
  margin: 12vh auto auto; max-height: 76dvh; overflow: hidden;
  flex-direction: column;   /* direction here, display in the dialog[open] rule */
}
.command::backdrop { background: rgb(0 0 0 / .5); backdrop-filter: blur(3px); }
.command input {
  width: 100%; border: 0; border-bottom: 1px solid var(--line); background: none;
  padding: var(--s4) var(--s5); font-size: max(var(--t-lg), 16px); flex: none;
}
.command input:focus { outline: 0; }
.command .menu { border: 0; box-shadow: none; min-width: 0; max-width: none; padding: 6px; overflow-y: auto; flex: 1; }
@media (max-width: 640px) {
  .command { margin-top: max(var(--s4), var(--safe-t)); width: calc(100vw - 24px); max-height: 82dvh; }
}

/* ── Skeletons & scrollers ────────────────────────────────────────────── */
/* A block that pulses slowly, not a sweeping shimmer. A sweeping shimmer is
   a moving gradient — the thing most often used to make a page look
   "advanced" while telling you nothing about what is being waited for. */
.skeleton {
  background: var(--paper-3); border-radius: var(--r-sm);
  animation: pulse 1.6s var(--ease) infinite;
}
@keyframes pulse { 0%, 100% { opacity: 1; } 50% { opacity: .55; } }
.skeleton-text { height: 12px; margin-block: 6px; }
.skeleton-circle { border-radius: 50%; }

.scroller { max-height: 240px; overflow-y: auto; overscroll-behavior: contain; }
.scroller::-webkit-scrollbar { width: 10px; }
.scroller::-webkit-scrollbar-thumb { background: var(--line-2); border-radius: 99px; border: 3px solid var(--paper); }
.resizable { resize: vertical; overflow: auto; min-height: 120px; }

/* ── Carousel: scroll-snap, not script ────────────────────────────────── */
.carousel {
  display: grid; grid-auto-flow: column; grid-auto-columns: min(78%, 320px);
  gap: var(--s3); overflow-x: auto; scroll-snap-type: x mandatory;
  scrollbar-width: none; padding-bottom: 4px;
  overscroll-behavior-x: contain;
}
.carousel::-webkit-scrollbar { display: none; }
.carousel > * { scroll-snap-align: start; }
@media (max-width: 640px) {
  .carousel { margin-inline: calc(var(--gutter) * -1); padding-inline: var(--gutter); }
}

/* ── Toolbar ──────────────────────────────────────────────────────────────
   On a narrow screen its contents scroll sideways rather than folding into
   three rows that eat half the screen. */
.toolbar {
  display: flex; align-items: center; gap: var(--s2); flex-wrap: wrap;
  padding: var(--s3) var(--s4); border: 1px solid var(--line); border-bottom: 0;
  border-radius: var(--r-card) var(--r-card) 0 0; background: var(--paper-2);
}
.toolbar + .tablewrap { border-radius: 0 0 var(--r-card) var(--r-card); }
@media (max-width: 640px) {
  .toolbar { flex-wrap: nowrap; overflow-x: auto; scrollbar-width: none; }
  .toolbar::-webkit-scrollbar { display: none; }
  .toolbar > * { flex: none; }
  .toolbar > .input { flex: 1 1 140px; min-width: 140px; }
}

/* Filter bar: a row of chips that can be switched on */
.filterbar { display: flex; gap: var(--s2); overflow-x: auto; scrollbar-width: none; padding-bottom: 2px; }
.filterbar::-webkit-scrollbar { display: none; }
.filterbar > * { flex: none; }
.filterbar [aria-pressed="true"] { background: var(--ink); color: var(--surface); box-shadow: none; }

/* ── Ratio & media ────────────────────────────────────────────────────── */
.ratio { position: relative; width: 100%; background: var(--paper-3); border-radius: var(--r-card); overflow: hidden; }
.ratio-16-9 { aspect-ratio: 16 / 9; }
.ratio-1-1 { aspect-ratio: 1 / 1; }
.ratio-4-3 { aspect-ratio: 4 / 3; }
.ratio > * { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; }

/* ── Calendar ─────────────────────────────────────────────────────────────
   Stretches with its container, so it fits a narrow card and a wide panel
   alike. Its cells are kept square with aspect-ratio, not a fixed height. */
.calendar { display: grid; gap: var(--s3); width: 100%; max-width: 340px; }
.calendar-head { display: flex; align-items: center; gap: var(--s2); }
.calendar-grid { display: grid; grid-template-columns: repeat(7, minmax(0, 1fr)); gap: 2px; }
.calendar-grid span {
  display: grid; place-items: center; aspect-ratio: 1; min-height: 34px;
  border-radius: 10px; font-size: var(--t-sm); color: var(--ink);
}
.calendar-grid .dow { color: var(--ink-3); font-size: var(--t-xs); font-weight: 640; aspect-ratio: auto; min-height: 22px; }
.calendar-grid .off { color: var(--ink-3); }
.calendar-grid .today { box-shadow: inset 0 0 0 var(--b) var(--line-2); }
.calendar-grid .on { background: var(--accent); color: var(--on-accent); font-weight: 640; }
.calendar-grid .in { background: var(--accent-soft); border-radius: 0; }
@media (hover: hover) { .calendar-grid span:not(.dow):hover { background: var(--paper-2); } }

/* ── Rules & odds and ends ────────────────────────────────────────────── */
.sep { display: flex; align-items: center; gap: var(--s3); color: var(--ink-3); font-size: var(--t-xs); }
.sep::before, .sep::after { content: ""; flex: 1; height: 1px; background: var(--line); }
.kbd {
  display: inline-grid; place-items: center; min-width: 22px; height: 22px; padding: 0 6px;
  border-radius: 6px; background: var(--paper-2); box-shadow: inset 0 0 0 1px var(--line-2);
  font: 600 11px/1 var(--mono); color: var(--ink-2);
}
.avatars { display: flex; }
.avatars .avatar + .avatar { margin-left: -10px; box-shadow: 0 0 0 2px var(--paper); }
.meter { display: flex; gap: 3px; }
.meter i { flex: 1; height: 6px; border-radius: 2px; background: var(--paper-3); }
.meter i.on { background: var(--accent); }

/* ── Passing messages ─────────────────────────────────────────────────────
   Placed above the iPhone home line, with room for one undo button — in this
   system undo is preferred over a confirmation box. */
.toast {
  position: fixed; left: 50%; bottom: calc(24px + var(--safe-b));
  transform: translate(-50%, 8px);
  display: flex; align-items: center; gap: var(--s3);
  width: max-content; max-width: calc(100vw - 32px);
  background: var(--ink); color: var(--surface);
  padding: 12px 18px; border-radius: var(--r-btn); font-size: var(--t-sm); font-weight: 560;
  opacity: 0; pointer-events: none; z-index: var(--z-toast);
  transition: opacity var(--d-mid) var(--ease), transform var(--d-mid) var(--ease);
}
.toast.is-on { opacity: 1; transform: translate(-50%, 0); pointer-events: auto; }
.toast button {
  border: 0; background: none; cursor: pointer; flex: none;
  color: var(--surface); font: 640 var(--t-sm)/1 var(--sans);
  text-decoration: underline; text-underline-offset: 3px;
  padding: 6px 2px; margin: -6px 0;
}
.toast .dot { background: currentColor; }
.toast-ok .dot { background: var(--green); }
.toast-err .dot { background: var(--red); }
@media (max-width: 640px) {
  .toast { left: var(--s4); right: var(--s4); transform: translateY(8px); width: auto; max-width: none; }
  .toast.is-on { transform: none; }
}

/* The saved marker — for pages that save themselves rather than using a
   Save button. Small, quiet, and it covers nothing. */
.saved { display: inline-flex; align-items: center; gap: 6px; font-size: var(--t-xs); color: var(--ink-3); font-weight: 560; }
.saved::before {
  content: ""; width: 12px; height: 9px; flex: none;
  border-left: var(--b) solid var(--grass); border-bottom: var(--b) solid var(--grass);
  transform: rotate(-45deg) translate(1px, -1px);
}
.saved.is-saving::before {
  width: 8px; height: 8px; border: 0; border-radius: 50%;
  background: var(--ink-3); transform: none; animation: pulse 1.1s var(--ease) infinite;
}

/* ═══════════════════════════════════════════════════════════════════════
   Data & reading numbers
   The charts here are small and library-free on purpose: what is wanted is
   the shape of a trend, not a full set of axes. Colour is used as a SCALE
   (one colour, several steps) — which is different from the decorative
   gradients that are banned, because every step here has a meaning you can
   read.
   ═══════════════════════════════════════════════════════════════════════ */

/* A tiny trend line. Its content is <svg viewBox="0 0 100 30"> with one
   polyline; the stroke is held at 1.5px whatever the width. */
.sparkline { display: block; width: 100%; height: 34px; overflow: visible; }
.sparkline polyline, .sparkline path { fill: none; stroke: var(--accent); stroke-width: 1.5; stroke-linecap: round; stroke-linejoin: round; vector-effect: non-scaling-stroke; }
.sparkline .area { fill: var(--accent-soft); stroke: none; }
.sparkline .last { fill: var(--accent); stroke: none; }

/* Labelled horizontal bars — the most honest comparison for categories.
   The length is set with style="--v: 72" (percent). */
.bars { display: grid; gap: var(--s3); }
.bars .bar { display: grid; grid-template-columns: minmax(64px, 22%) minmax(0, 1fr) auto; gap: var(--s3); align-items: center; font-size: var(--t-sm); }
.bars .bar > b { font-weight: 560; color: var(--ink-2); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.bars .bar > i { display: block; height: 10px; border-radius: 3px; background: var(--paper-3); position: relative; }
.bars .bar > i::after {
  content: ""; position: absolute; inset: 0 auto 0 0; width: calc(var(--v, 0) * 1%);
  background: var(--accent); border-radius: 3px;
  transition: width var(--d-mid) var(--ease);
}
.bars .bar > span { font: 600 var(--t-xs)/1 var(--mono); font-variant-numeric: tabular-nums; color: var(--ink-2); }
@media (max-width: 480px) {
  .bars .bar { grid-template-columns: minmax(0, 1fr) auto; grid-template-areas: "label value" "track track"; row-gap: 5px; }
  .bars .bar > b { grid-area: label; } .bars .bar > span { grid-area: value; } .bars .bar > i { grid-area: track; }
}

/* A progress ring. Two SVG circles; the value comes from style="--v: 68". */
.ring { display: inline-grid; place-items: center; position: relative; width: var(--size, 84px); height: var(--size, 84px); }
.ring svg { width: 100%; height: 100%; transform: rotate(-90deg); }
.ring circle { fill: none; stroke-width: var(--thick, 8); }
.ring .track { stroke: var(--paper-3); }
.ring .val {
  stroke: var(--accent); stroke-linecap: round;
  stroke-dasharray: var(--circ, 251.2);
  stroke-dashoffset: calc(var(--circ, 251.2) - (var(--circ, 251.2) * var(--v, 0) / 100));
  transition: stroke-dashoffset var(--d-mid) var(--ease);
}
.ring .ring-label { position: absolute; font: 660 var(--t-md)/1 var(--sans); font-variant-numeric: tabular-nums; }

/* A daily density map, like a contribution board. There are five steps, and
   every step must be explainable — not a random colour. */
.heat { display: grid; grid-auto-flow: column; grid-template-rows: repeat(7, 1fr); gap: 3px; overflow-x: auto; scrollbar-width: none; padding-bottom: 2px; }
.heat::-webkit-scrollbar { display: none; }
.heat i { width: 12px; height: 12px; border-radius: 3px; background: var(--paper-3); }
.heat i[data-l="1"] { background: color-mix(in srgb, var(--accent) 26%, var(--paper-2)); }
.heat i[data-l="2"] { background: color-mix(in srgb, var(--accent) 50%, var(--paper-2)); }
.heat i[data-l="3"] { background: color-mix(in srgb, var(--accent) 74%, var(--paper-2)); }
.heat i[data-l="4"] { background: var(--accent); }
.heat-key { display: inline-flex; align-items: center; gap: 4px; font-size: var(--t-xs); color: var(--ink-3); }

/* Term and value pairs. On a phone they drop to two lines; do not force
   them side by side until the value is squeezed. */
.dl { display: grid; gap: 0; }
.dl > div { display: grid; grid-template-columns: minmax(120px, 32%) minmax(0, 1fr); gap: var(--s4); padding: var(--s3) 0; border-top: 1px solid var(--line); }
.dl > div:first-child { border-top: 0; }
.dl dt { font-size: var(--t-sm); color: var(--ink-3); }
.dl dd { font-size: var(--t-sm); min-width: 0; overflow-wrap: break-word; }
@media (max-width: 480px) { .dl > div { grid-template-columns: minmax(0, 1fr); gap: 2px; } }

/* A timeline — events in order with a connecting line. */
.timeline { display: grid; gap: 0; }
.timeline > li { display: grid; grid-template-columns: 22px minmax(0, 1fr); gap: var(--s3); padding-bottom: var(--s5); position: relative; list-style: none; }
.timeline > li::before {
  content: ""; position: absolute; left: 10px; top: 20px; bottom: -2px;
  width: var(--b); background: var(--line);
}
.timeline > li:last-child { padding-bottom: 0; }
.timeline > li:last-child::before { display: none; }
.timeline .pip {
  width: 22px; height: 22px; border-radius: 50%; display: grid; place-items: center;
  background: var(--paper-2); box-shadow: inset 0 0 0 var(--b) var(--line-2);
  font-size: 11px; z-index: 1;
}
.timeline .pip-on { background: var(--accent); color: var(--on-accent); box-shadow: none; }
.timeline .when { font: 500 var(--t-xs)/1.4 var(--mono); color: var(--ink-3); }

/* Stepped progress. Vertical on a phone — six steps side by side at 375px
   leave about three letters per label. */
.steps { display: flex; gap: var(--s2); counter-reset: step; }
.steps > li { flex: 1; display: grid; gap: 6px; list-style: none; min-width: 0; }
.steps > li::before {
  content: ""; display: block; height: 4px; border-radius: 2px; background: var(--paper-3);
}
.steps > li .step-t { font-size: var(--t-xs); font-weight: 620; color: var(--ink-3); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.steps > li[data-state="done"]::before { background: var(--accent); }
.steps > li[data-state="done"] .step-t { color: var(--ink-2); }
.steps > li[data-state="now"]::before { background: var(--accent); }
.steps > li[data-state="now"] .step-t { color: var(--ink); }
@media (max-width: 640px) {
  .steps { flex-direction: column; gap: var(--s3); }
  .steps > li { display: grid; grid-template-columns: 4px minmax(0, 1fr); gap: var(--s3); align-items: center; }
  .steps > li::before { height: 100%; min-height: 20px; width: 4px; }
  .steps > li .step-t { white-space: normal; font-size: var(--t-sm); }
}

/* A column board. The columns scroll sideways on a phone — that is how
   people actually use a board on a small screen, rather than stacking them
   into one long column. */
.kanban { display: grid; grid-auto-flow: column; grid-auto-columns: minmax(260px, 1fr); gap: var(--s4); overflow-x: auto; padding-bottom: var(--s2); align-items: start; }
.kanban::-webkit-scrollbar { height: 8px; }
.kanban::-webkit-scrollbar-thumb { background: var(--line-2); border-radius: 99px; }
.kanban .col { background: var(--paper-2); border-radius: var(--r-card); padding: var(--s3); display: grid; gap: var(--s2); align-content: start; }
.kanban .col > h3 { display: flex; align-items: center; gap: var(--s2); font-size: var(--t-xs); font-weight: 660; letter-spacing: .06em; text-transform: uppercase; color: var(--ink-3); padding: var(--s1) var(--s2); }
.kanban .col > h3 .count { margin-left: auto; font-family: var(--mono); }
.kanban .item { background: var(--surface); border: 1px solid var(--line); border-radius: var(--r-btn); padding: var(--s3); display: grid; gap: var(--s2); font-size: var(--t-sm); cursor: grab; }
@media (max-width: 640px) { .kanban { grid-auto-columns: min(80%, 300px); margin-inline: calc(var(--gutter) * -1); padding-inline: var(--gutter); } }

/* A file tree — riding on <details>, so opening and closing is correct with
   no script. */
.tree { font-size: var(--t-sm); }
.tree ul { list-style: none; padding-left: var(--s4); margin: 0; border-left: 1px solid var(--line); }
.tree li { padding: 1px 0; }
.tree summary, .tree .leaf {
  display: flex; align-items: center; gap: var(--s2); padding: 5px var(--s2);
  border-radius: var(--r-sm); cursor: pointer; list-style: none; min-height: 32px;
}
.tree summary::-webkit-details-marker { display: none; }
.tree summary::before { content: "▸"; color: var(--ink-3); font-size: 10px; transition: transform var(--d-fast) var(--ease); }
.tree details[open] > summary::before { transform: rotate(90deg); }
.tree .leaf { padding-left: calc(var(--s2) + 14px); color: var(--ink-2); text-decoration: none; }
@media (hover: hover) { .tree summary:hover, .tree .leaf:hover { background: var(--paper-2); } }
.tree [aria-current="true"] { background: var(--accent-soft); color: var(--accent); font-weight: 600; }
@media (pointer: coarse) { .tree summary, .tree .leaf { min-height: 40px; } }

/* Terminal output / a running log. Long lines are clipped, not widened. */
.logview {
  font: 12px/1.65 var(--mono); background: var(--paper-2);
  border: 1px solid var(--line); border-radius: var(--r-card);
  padding: var(--s3) 0; max-height: 320px; overflow: auto; overscroll-behavior: contain;
}
.logview div { display: grid; grid-template-columns: auto minmax(0, 1fr); gap: var(--s3); padding: 1px var(--s4); white-space: pre-wrap; overflow-wrap: anywhere; }
@media (hover: hover) { .logview div:hover { background: var(--paper-3); } }
.logview .t { color: var(--ink-3); user-select: none; }
.logview .warn { color: var(--orange); }
.logview .err { color: var(--red); }
.logview .ok { color: var(--green); }

/* A diff — added, removed, context. */
.diff { font: 12px/1.7 var(--mono); border: 1px solid var(--line); border-radius: var(--r-card); overflow: auto; }
.diff div { display: grid; grid-template-columns: 44px minmax(0, 1fr); white-space: pre-wrap; overflow-wrap: anywhere; }
.diff .ln { text-align: right; padding-right: var(--s3); color: var(--ink-3); user-select: none; background: var(--paper-2); }
.diff .add { background: color-mix(in srgb, var(--green) 12%, var(--surface)); }
.diff .add .ln { background: color-mix(in srgb, var(--green) 20%, var(--paper-2)); }
.diff .del { background: color-mix(in srgb, var(--red) 12%, var(--surface)); }
.diff .del .ln { background: color-mix(in srgb, var(--red) 20%, var(--paper-2)); }

/* ═══════════════════════════════════════════════════════════════════════
   Page patterns
   Shapes that repeat in nearly every app. They live in the system so they
   are not reinvented each time with slightly different spacing and sizes.
   ═══════════════════════════════════════════════════════════════════════ */

/* Search with suggestions. The list is plain elements, so a screen reader
   can read it and the arrow keys work. */
.combo { position: relative; }
.combo .combo-list {
  position: absolute; top: calc(100% + 6px); left: 0; right: 0; z-index: var(--z-pop);
  max-height: 264px; overflow-y: auto; overscroll-behavior: contain;
}
.combo .combo-list[hidden] { display: none; }
.combo mark { background: var(--accent-soft); color: var(--accent); font-weight: 640; border-radius: 3px; }
.combo .combo-empty { padding: var(--s4); text-align: center; color: var(--ink-3); font-size: var(--t-sm); }

/* A settings row: the explanation on the left, one control on the right. */
.settingrow {
  display: flex; align-items: center; justify-content: space-between; gap: var(--s4);
  padding: var(--s4) 0; border-top: 1px solid var(--line);
}
.settingrow:first-child { border-top: 0; }
.settingrow .txt { min-width: 0; display: grid; gap: 2px; }
.settingrow .txt b { font-weight: 620; font-size: var(--t-sm); }
.settingrow .txt span { font-size: var(--t-xs); color: var(--ink-3); }
.settingrow .ctl { flex: none; }
@media (max-width: 480px) {
  .settingrow { flex-direction: column; align-items: stretch; }
  .settingrow .ctl { display: flex; justify-content: flex-start; }
}

/* Conversation bubbles */
.chat { display: grid; gap: var(--s3); }
.chat .msg { display: grid; gap: 4px; max-width: min(78%, 560px); }
.chat .bubble { padding: 10px var(--s4); border-radius: var(--r-lg); font-size: var(--t-sm); line-height: 1.5; overflow-wrap: break-word; }
.chat .msg-in { justify-self: start; }
.chat .msg-in .bubble { background: var(--paper-2); border-bottom-left-radius: var(--s1); }
.chat .msg-out { justify-self: end; }
.chat .msg-out .bubble { background: var(--accent); color: var(--on-accent); border-bottom-right-radius: var(--s1); }
.chat .when { font-size: 11px; color: var(--ink-3); font-family: var(--mono); }
.chat .msg-out .when { justify-self: end; }
.chat .typing { display: inline-flex; gap: 3px; padding: 14px var(--s4); background: var(--paper-2); border-radius: var(--r-lg); width: max-content; }
.chat .typing i { width: 6px; height: 6px; border-radius: 50%; background: var(--ink-3); animation: blink 1.2s infinite; }
.chat .typing i:nth-child(2) { animation-delay: .18s; }
.chat .typing i:nth-child(3) { animation-delay: .36s; }
@keyframes blink { 0%, 60%, 100% { opacity: .3; } 30% { opacity: 1; } }
@media (max-width: 480px) { .chat .msg { max-width: 88%; } }

/* A comment thread — replies indent one level only; more than that does not
   read on a phone. */
.comment { display: grid; grid-template-columns: auto minmax(0, 1fr); gap: var(--s3); }
.comment .body { display: grid; gap: var(--s2); min-width: 0; }
.comment .meta { display: flex; align-items: baseline; gap: var(--s2); flex-wrap: wrap; font-size: var(--t-sm); }
.comment .meta b { font-weight: 620; }
.comment .meta .when { color: var(--ink-3); font-size: var(--t-xs); }
.comment .txt { font-size: var(--t-sm); color: var(--ink-2); overflow-wrap: break-word; }
.comment .acts { display: flex; gap: var(--s3); }
.comment .acts button { border: 0; background: none; cursor: pointer; padding: 4px 0; color: var(--ink-3); font: 600 var(--t-xs)/1 var(--sans); }
@media (hover: hover) { .comment .acts button:hover { color: var(--ink); } }
.comment .replies { display: grid; gap: var(--s4); margin-top: var(--s4); padding-left: var(--s4); border-left: 1px solid var(--line); }

/* Pricing cards. The recommended one is raised a little and given an accent
   line — one only, never both of them emphasised. */
.pricing { display: grid; gap: var(--s4); grid-template-columns: repeat(auto-fit, minmax(min(100%, 260px), 1fr)); align-items: stretch; }
.pricing .plan { display: flex; flex-direction: column; gap: var(--s4); }
.pricing .plan .price { font-size: var(--t-3xl); font-weight: 680; letter-spacing: -0.03em; line-height: 1; font-variant-numeric: tabular-nums; }
.pricing .plan .price small { font-size: var(--t-sm); font-weight: 500; color: var(--ink-3); letter-spacing: 0; }
.pricing .plan ul { list-style: none; display: grid; gap: var(--s2); font-size: var(--t-sm); color: var(--ink-2); }
.pricing .plan ul li { display: grid; grid-template-columns: 16px minmax(0, 1fr); gap: var(--s2); }
.pricing .plan ul li::before { content: "✓"; color: var(--green); font-weight: 700; }
.pricing .plan ul li.off { color: var(--ink-3); }
.pricing .plan ul li.off::before { content: "—"; color: var(--ink-3); }
.pricing .plan .btn { margin-top: auto; }
.pricing .plan-hi { border-color: var(--accent); box-shadow: inset 0 0 0 1px var(--accent); }

/* A row of partner logos — deliberately flattened to one colour, so it does
   not become a colour show in the middle of the page. */
.logos { display: flex; flex-wrap: wrap; align-items: center; gap: var(--s6); justify-content: center; }
.logos > * { color: var(--ink-3); font-weight: 660; font-size: var(--t-lg); letter-spacing: -0.02em; opacity: .75; }
.logos img { filter: grayscale(1); opacity: .55; max-height: 26px; width: auto; }

/* Page footer */
.footer { border-top: 1px solid var(--line); padding-block: var(--s7); padding-bottom: max(var(--s7), var(--safe-b)); }
.footer .cols { display: grid; gap: var(--s5); grid-template-columns: repeat(auto-fit, minmax(min(100%, 160px), 1fr)); }
.footer h4 { font-size: var(--t-xs); font-weight: 660; letter-spacing: .08em; text-transform: uppercase; color: var(--ink-3); margin-bottom: var(--s3); }
.footer a { display: block; padding: 5px 0; color: var(--ink-2); text-decoration: none; font-size: var(--t-sm); }
@media (hover: hover) { .footer a:hover { color: var(--ink); } }
.footer .foot-end { display: flex; gap: var(--s4); flex-wrap: wrap; align-items: center; margin-top: var(--s6); padding-top: var(--s4); border-top: 1px solid var(--line); font-size: var(--t-xs); color: var(--ink-3); }

/* A list of keyboard shortcuts */
.shortcuts { display: grid; gap: 0; }
.shortcuts > div { display: flex; align-items: center; justify-content: space-between; gap: var(--s4); padding: 9px 0; border-top: 1px solid var(--line); font-size: var(--t-sm); }
.shortcuts > div:first-child { border-top: 0; }
.shortcuts .keys { display: flex; gap: 3px; flex: none; }

/* Who is here — avatars plus a state dot */
.presence { position: relative; display: inline-flex; }
.presence .dot {
  position: absolute; right: -1px; bottom: -1px; width: 11px; height: 11px;
  box-shadow: 0 0 0 2px var(--paper);
}

/* A table of contents pinned to the right of long reading */
.toc { position: sticky; top: calc(var(--h-topbar) + var(--s5)); align-self: start; display: grid; gap: 1px; font-size: var(--t-sm); }
.toc a { display: block; padding: 5px var(--s3); color: var(--ink-3); text-decoration: none; border-left: 2px solid var(--line); }
@media (hover: hover) { .toc a:hover { color: var(--ink); } }
.toc a[aria-current="true"] { color: var(--ink); border-left-color: var(--accent); font-weight: 600; }
.toc .lvl-3 { padding-left: var(--s5); font-size: var(--t-xs); }

/* Back to top — appears on its own once the page is scrolled far */
.backtop {
  position: fixed; right: max(var(--s4), var(--safe-r)); bottom: calc(var(--s4) + var(--safe-b));
  z-index: var(--z-pop); width: 44px; height: 44px; border-radius: 50%;
  display: grid; place-items: center; cursor: pointer;
  border: 1px solid var(--line); background: var(--surface); color: var(--ink);
  box-shadow: var(--shadow-2);
  opacity: 0; pointer-events: none; transform: translateY(8px);
  transition: opacity var(--d-mid) var(--ease), transform var(--d-mid) var(--ease);
}
.backtop.is-on { opacity: 1; pointer-events: auto; transform: none; }

/* Accent picker — used to set the page's --accent */
.swatchpick { display: flex; gap: var(--s2); flex-wrap: wrap; }
.swatchpick button {
  width: 32px; height: 32px; border-radius: var(--r-sm); cursor: pointer;
  border: 0; background: var(--c); box-shadow: inset 0 0 0 1px rgb(0 0 0 / .08);
}
.swatchpick button[aria-pressed="true"] { box-shadow: 0 0 0 2px var(--paper), 0 0 0 4px var(--ink); }
@media (pointer: coarse) { .swatchpick button { width: 40px; height: 40px; } }

/* ── Long reading ─────────────────────────────────────────────────────────
   One class for content coming from markdown or a CMS: there are no classes
   inside it, so everything is governed from here. */
.prose { max-width: var(--w-prose); font-size: var(--t-md); line-height: 1.7; }
.prose > * + * { margin-top: var(--s4); }
.prose h2 { font-size: var(--t-2xl); font-weight: 640; line-height: 1.15; letter-spacing: -0.025em; margin-top: var(--s7); }
.prose h3 { font-size: var(--t-xl); font-weight: 620; line-height: 1.25; letter-spacing: -0.02em; margin-top: var(--s6); }
.prose h2 + *, .prose h3 + * { margin-top: var(--s3); }
.prose p { color: var(--ink-2); }
.prose a { color: var(--accent); text-decoration: underline; }
.prose ul, .prose ol { padding-left: var(--s5); color: var(--ink-2); display: grid; gap: var(--s2); }
.prose li::marker { color: var(--ink-3); }
.prose blockquote { border-left: 3px solid var(--accent); padding-left: var(--s4); color: var(--ink-2); font-style: italic; }
.prose img { border-radius: var(--r-card); }
.prose hr { height: 1px; background: var(--line); border: 0; margin-block: var(--s6); }
.prose code { font: 0.875em/1.6 var(--mono); background: var(--paper-2); padding: 2px 6px; border-radius: 6px; }
.prose pre { font: 0.8125rem/1.7 var(--mono); background: var(--paper-2); border: 1px solid var(--line); border-radius: var(--r-card); padding: var(--s4); overflow-x: auto; }
.prose pre code { background: none; padding: 0; }
.prose table { width: 100%; border-collapse: collapse; font-size: var(--t-sm); display: block; overflow-x: auto; }
.prose th, .prose td { padding: var(--s2) var(--s3); border: 1px solid var(--line); text-align: left; }
.prose figcaption { font-size: var(--t-xs); color: var(--ink-3); margin-top: var(--s2); }

/* ═══════════════════════════════════════════════════════════════════════
   MOTION
   Lifted from foto.hanif.app — not the effects, but the REASONS. Every
   motion below explains something a still surface cannot: this can be
   picked up · the thing moved over here · this one just arrived · what you
   deleted went that way.

   One condition holds across this whole file, and it is what makes all of
   it detachable: ZERO animation runtime. No .animate(), no library. The
   JavaScript only sets classes and fills custom properties; what moves is
   still CSS. An app that never loads ui.js still gets every motion that
   does not need to know where the cursor is.
   ═══════════════════════════════════════════════════════════════════════ */

/* ── Press ────────────────────────────────────────────────────────────────
   On a touch screen there is no :hover at all. With no feedback on press, a
   control feels dead — people assume the tap did not register and press
   again and again. That is not about beauty, it is a failure mode.

   .btn is not in this list: it already has its own motion (the thinning
   underside), and stacking a scale on top makes two motions fight over one
   event. */
.navlink:active,
.chip[href]:active, button.chip:active,
.toggles button:active,
.crumbs a:active,
.pager button:active, .pager a:active,
.side a:active,
.bottomnav a:active {
  transform: scale(.97);
}
.navlink, .chip[href], button.chip, .toggles button, .crumbs a,
.pager button, .pager a, .side a, .bottomnav a {
  transition: transform var(--d-fast) var(--ease), background var(--d-fast) var(--ease), color var(--d-fast) var(--ease);
}

/* ── Lift ─────────────────────────────────────────────────────────────────
   A picture you can pick up. On hover it rises, tilts slightly towards the
   cursor, and gains a white margin like a photographic print.

   That print margin is written as a box-shadow spread, NOT as a border or
   padding — so it never shifts the layout by a single pixel.

   The tilt is filled in by ui.js through --rx/--ry. The 0deg fallback
   matters: with no JavaScript (or on keyboard focus, where there is no
   cursor position) the object still lifts correctly, it just does not
   tilt. */
[data-tilt] { perspective: 1400px; }
[data-tilt] > * { transform-style: preserve-3d; }
.lift {
  display: block;
  border-radius: var(--r-card);
  overflow: hidden;
  background: var(--mat);
  box-shadow: var(--shadow-s);
  transition: transform var(--d-mid) var(--ease), box-shadow var(--d-mid) var(--ease);
  will-change: transform;
}
.lift > img { display: block; width: 100%; transition: transform .5s var(--ease); }
@media (hover: hover) {
  .lift:hover, [data-tilt] > *:hover .lift {
    transform: translateY(-8px) scale(1.03) rotateX(var(--rx, 0deg)) rotateY(var(--ry, 0deg));
    box-shadow: 0 0 0 5px var(--print, rgb(255 255 255 / .92)), 0 24px 42px -14px rgb(0 0 0 / .6);
    z-index: 6;
  }
  /* The picture grows more slowly than its frame (0.5s against 0.18s). That
     difference in speed is what makes the image read as floating behind the
     frame rather than stuck on top of it. */
  .lift:hover > img, [data-tilt] > *:hover .lift > img { transform: scale(1.05); }
}
/* In the dark studios the print ring is dimmed slightly and the shadow
   deepened — full white on a dark ground reads as a lamp. */
[data-theme="dark"] .lift:hover,
[data-studio="graphite"] .lift:hover,
[data-studio="ink"] .lift:hover { --print: rgb(255 255 255 / .9); box-shadow: 0 0 0 5px rgb(255 255 255 / .9), 0 24px 46px -14px rgb(0 0 0 / .8); }

/* ── Pile ─────────────────────────────────────────────────────────────────
   A folder card whose contents show. Three images are stacked in exactly
   the same place, then each is clipped to a single one-third slice — so
   what reads is one seamless surface holding three photographs side by
   side, not three pictures pasted together.

   Why clip-path and not three boxes in a row: object-fit: cover works on
   each image's FULL canvas, so every slice shows the middle band of its
   photograph — the part most likely to hold the subject. Cut into three
   narrow columns instead, each photo is cropped to a thin strip that
   almost never contains anything.

   With one or two images it deliberately shows one full-bleed (using
   .solo): two slices read as a broken card, not as a folder that happens
   to hold two photographs. */
.pile {
  position: relative;
  display: block;
  width: 100%;
  aspect-ratio: 3 / 2;
  border-radius: var(--r-sm);
  overflow: hidden;
  background: var(--mat);
}
.pile > img {
  position: absolute; inset: 0;
  width: 100%; height: 100%;
  object-fit: cover;
  /* The stack itself never moves. What moves is the card holding it — if the
     slices shifted too, the illusion of one surface would break. */
  transition: none;
}
.pile > img:nth-child(1) { clip-path: inset(0 66.7% 0 0); }
.pile > img:nth-child(2) { clip-path: inset(0 33.4% 0 33.3%); }
.pile > img:nth-child(3) { clip-path: inset(0 0 0 66.6%); }
.pile.solo > img { clip-path: none; }
.pile .blank { position: absolute; inset: 0; display: grid; place-items: center; color: var(--ink-3); opacity: .5; }

/* The card that holds the pile. Kept apart from .card because its shape is
   genuinely different: the image runs to the edges, the label sits below. */
.folder {
  position: relative;
  display: block;
  padding: var(--s2);
  border: var(--b) solid var(--line);
  border-radius: var(--r-card);
  background: var(--surface);
  box-shadow: var(--shadow-s);
  color: inherit;
  text-align: left;
  text-decoration: none;
  cursor: pointer;
  transition: transform var(--d-mid) var(--ease), box-shadow var(--d-mid) var(--ease), border-color var(--d-mid) var(--ease);
}
.folder .info { display: block; padding: var(--s2) var(--s1) 2px; min-width: 0; }
.folder .info b { display: block; font-size: var(--t-sm); font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.folder .info small { font: 500 var(--t-meta)/1.4 var(--mono); letter-spacing: .06em; text-transform: uppercase; color: var(--ink-3); }
@media (hover: hover) {
  .folder:hover { transform: translateY(-1px); box-shadow: var(--shadow-m); border-color: color-mix(in srgb, var(--accent) 34%, var(--line)); }
}
.folder:active { transform: translateY(0); box-shadow: var(--shadow-s); }
/* The menu button in the corner: appears on hover. On a touch screen
   :hover never happens, and this is the only way into the folder menu — if
   it stayed transparent, that menu would be impossible to open. */
.folder .act {
  position: absolute; top: var(--s3); right: var(--s3); z-index: 2;
  width: 28px; height: 28px; display: grid; place-items: center;
  border: var(--b) solid var(--line); border-radius: var(--r-sm);
  background: var(--surface); color: var(--ink-2); cursor: pointer;
  opacity: 0; transition: opacity var(--d-fast) var(--ease);
}
.folder:hover .act, .folder:focus-within .act { opacity: 1; }
@media (hover: none) { .folder .act { opacity: 1; width: 40px; height: 40px; } }

/* ── Rail ─────────────────────────────────────────────────────────────────
   A row of cards scrolled sideways. The padding and the negative margin are
   deliberately paired: that is what gives a lifted card room for its shadow
   without adding height to the row, and without the shadow being clipped by
   the overflow. */
.rail {
  display: flex; gap: var(--s3);
  overflow-x: auto;
  padding: 4px 4px var(--s3);
  margin: -4px -4px calc(var(--s3) * -1);
  scroll-snap-type: x proximity;
  scrollbar-width: thin;
}
.rail > * { flex: 0 0 auto; width: 200px; scroll-snap-align: start; }
.rail::-webkit-scrollbar { height: 6px; }
.rail::-webkit-scrollbar-thumb { border-radius: var(--r-pill); background: var(--line); }

/* ── Arriving ─────────────────────────────────────────────────────────────
   Images in a grid. What moves is the PICTURE, not its frame.

   The reason comes from a real failure: each tile used to be animated
   upward before its image had loaded, and on a real network that read as
   "the frame appears first, the picture follows" — one object arriving
   twice. Now the frame is still from the start; the picture fades in when
   it is genuinely ready. One movement, no flicker.

   ui.js adds .ld when an image is ready. Anything already in cache is
   marked without a transition, so re-rendering does not make a picture
   flicker through its fade again. */
.fade-img { opacity: 0; transition: opacity .32s var(--ease); }
.fade-img.ld { opacity: 1; }

/* A grid that explains a change. Used ONLY on the trigger — a new sort, a
   new filter, a new view — never on every render. The 12ms stagger is cut
   off at the 24th tile: past that the eye has the pattern, and waiting
   longer is just a delay. */
.grid-anim > * {
  animation: tile-in .34s var(--ease) backwards;
  animation-delay: calc(min(var(--i, 0), 24) * 12ms);
}
@keyframes tile-in { from { opacity: 0; transform: translateY(8px) scale(.98); } }

/* What has just arrived gets one highlight. Its accent ring is held until
   40% before fading — that is what gives the eye time to find it before it
   settles in with the rest. This is the "mine is in" moment. */
.is-new { animation: land 1.4s var(--ease); }
@keyframes land {
  0%   { opacity: 0; transform: translateY(14px) scale(.96); box-shadow: 0 0 0 3px var(--accent); }
  40%  { box-shadow: 0 0 0 3px var(--accent); }
  100% { opacity: 1; transform: none; box-shadow: var(--shadow-s); }
}

/* What leaves, leaves in a direction. Its colour drains first and then the
   object shrinks — rather than vanishing raw, which startles. The caller
   MUST wait out this duration before lifting it from the DOM; ui.js
   provides `release()` so the number is never written twice. */
.is-leaving { animation: tile-out .25s var(--ease) forwards; pointer-events: none; }
@keyframes tile-out { to { opacity: 0; transform: scale(.82); filter: saturate(.6); } }

/* ── Drag ─────────────────────────────────────────────────────────────────
   A ghost of the object following the cursor. Tilted 2° when picked up,
   STRAIGHT when held over a target — that is what says "let go here and it
   goes in", without a single word.

   Four states with different durations, and that is deliberate: picking up
   and letting go feel quick, but the object entering its target (.enter)
   is given longer so the eye can follow where the thing went. */
.ghost {
  position: fixed; z-index: var(--z-overlay);
  pointer-events: none;
  border-radius: var(--r-card);
  background-size: cover; background-position: center;
  box-shadow: var(--shadow-l);
  opacity: .96;
  transform: scale(1) rotate(-2deg);
  will-change: left, top, transform;
  transition: transform var(--d-mid) var(--ease), opacity var(--d-mid) var(--ease);
}
.ghost.small  { transform: scale(.5) rotate(0deg); }
.ghost.enter  { transition: transform .42s var(--ease), opacity .42s var(--ease); transform: scale(.32) rotate(0deg); opacity: 0; }
.ghost.cancel  { transition: left .24s var(--ease), top .24s var(--ease), opacity .24s var(--ease); opacity: 0; }
.ghost .n {
  position: absolute; top: -8px; right: -8px;
  min-width: 22px; height: 22px; padding: 0 6px;
  display: grid; place-items: center; border-radius: var(--r-pill);
  background: var(--accent); color: var(--on-accent);
  font: 600 var(--t-meta)/1 var(--mono);
}
/* Two levels of clarity, not one: EVERY possible target is marked faintly
   as soon as the drag starts, then the one being aimed at lights up fully.
   Without the first level, people do not know where this may be dropped. */
.is-dragging [data-drop] { outline: 1.5px dashed color-mix(in srgb, var(--accent) 55%, transparent); outline-offset: 2px; }
.is-dragging { cursor: grabbing; user-select: none; }
.is-dragging img { -webkit-user-drag: none; }
[data-drop].aim { outline: 2px solid var(--accent); outline-offset: 2px; background: var(--accent-soft); box-shadow: 0 0 0 4px var(--accent-soft); }
[data-drop].accept { animation: accept .5s var(--ease); }
@keyframes accept {
  0%   { box-shadow: 0 0 0 0 var(--accent); }
  45%  { box-shadow: 0 0 0 5px var(--accent-soft); }
  100% { box-shadow: 0 0 0 0 transparent; }
}

/* ── Peek ─────────────────────────────────────────────────────────────────
   Rest the cursor on something for a moment and its contents appear —
   without clicking and losing your place. The delay is 520ms: long enough
   not to fire when the cursor is merely passing, short enough not to feel
   like waiting.

   Its transform-origin is filled in by ui.js according to which side the
   panel appears on, so it reads as GROWING FROM the thing being peeked at —
   not appearing from nowhere and happening to land nearby.

   Touch devices have no "point without pressing", so the whole component is
   turned off there — not shrunk, turned off. */
.peek {
  position: fixed; z-index: var(--z-pop);
  width: min(340px, 82vw);
  border: var(--b) solid var(--line);
  border-radius: var(--r-card);
  background: var(--surface);
  box-shadow: var(--shadow-l);
  overflow: hidden;
  pointer-events: none;
  opacity: 0;
  transform: scale(.97);
  transform-origin: var(--origin, left center);
  transition: opacity .14s var(--ease), transform .16s var(--ease);
}
.peek.is-on { opacity: 1; transform: none; }
.peek > img { display: block; width: 100%; max-height: 60vh; object-fit: contain; background: var(--mat); }
.peek .caption { padding: var(--s3); border-top: var(--b) solid var(--line-2); }
.peek .caption b { display: block; font-size: var(--t-sm); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.peek .caption .meta { margin-top: 2px; }
@media (pointer: coarse) { .peek { display: none !important; } }

/* ── Folds that actually move ─────────────────────────────────────────────
   The open/close marker has been animated for a long time, but the BODY
   jumped — so the icon was smooth while its content snapped. Half-done
   reads as more wrong than not done at all.

   interpolate-size makes `height: auto` animatable; a browser without it
   simply ignores this block and falls back to the old behaviour, so nothing
   breaks. */
@supports (interpolate-size: allow-keywords) {
  :root { interpolate-size: allow-keywords; }
  .accordion details::details-content,
  .disclosure details::details-content,
  details.fold::details-content {
    block-size: 0;
    overflow: hidden;
    transition: block-size var(--d-mid) var(--ease), content-visibility var(--d-mid) allow-discrete;
    transition-behavior: allow-discrete;
  }
  .accordion details[open]::details-content,
  .disclosure details[open]::details-content,
  details.fold[open]::details-content { block-size: auto; }
}

/* ── Things that used to jump with no explanation ─────────────────────────
   A consistency fix, not an addition. All of these already changed colour
   when their state changed — the change was just abrupt, while their
   neighbours in the same role (.tab, .toggles) were already smooth. */
.toc a { transition: color var(--d-fast) var(--ease), border-left-color var(--d-fast) var(--ease); }
.table tbody tr, .tree summary, .tree .leaf, .logview div,
.calendar-grid span:not(.dow), .menu button, .menu a,
.footer a, .comment .acts button, .filterbar button, .steps li::before, .meter i {
  transition: background var(--d-fast) var(--ease), color var(--d-fast) var(--ease), border-color var(--d-fast) var(--ease);
}
/* The command palette never got an entry animation because its markup is
   <dialog class="command">, not .dialog — so it appeared out of nowhere,
   even though it is the one opened most often (⌘K). */
.command[open] { animation: dialog-in var(--d-mid) var(--ease); }
.command .is-cur { transition: background var(--d-fast) var(--ease); }

/* Hints and hover cards that move must be fenced in: on a touch screen
   :hover sticks after the finger lifts, leaving the panel on screen. These
   two had slipped past the system's own rule. */
.tip > .tiptext, .hovercard > .hovercard-panel { transition-property: opacity, visibility; }
@media (hover: hover) {
  .tip > .tiptext, .hovercard > .hovercard-panel { transition-property: opacity, transform, visibility; }
}

@media (prefers-reduced-motion: reduce) {
  /* What is switched off here is the DISTANCE, not the existence: things
     still appear and disappear, they just do not travel. The "just arrived"
     highlight is kept as a still ring — it carries information, and that
     information must not vanish along with its motion. */
  .lift:hover, [data-tilt] > *:hover .lift { transform: none; }
  .lift > img, .lift:hover > img, [data-tilt] > *:hover .lift > img { transform: none; }
  .folder:hover, .folder:active { transform: none; }
  .navlink:active, .chip[href]:active, button.chip:active, .toggles button:active,
  .crumbs a:active, .pager button:active, .pager a:active, .side a:active, .bottomnav a:active { transform: none; }
  .grid-anim > * { animation: none; }
  .fade-img { transition: none; }
  .is-new { animation: none; box-shadow: 0 0 0 3px var(--accent); }
  .is-leaving { animation: none; opacity: 0; }
  .ghost, .ghost.small, .ghost.enter, .ghost.cancel { transition: opacity var(--d-mid) var(--ease); transform: none; }
  .peek { transition: opacity var(--d-fast) var(--ease); transform: none; }
  .accordion details::details-content, .disclosure details::details-content,
  details.fold::details-content { transition: none; }
}


/* ─────────────────────────────────────────────────────────────────────────
   hanif ui · page shells & frames
   One decision made once, at the start: which kind of page is this. Every-
   thing in this file is about SPACE — widths, columns, distances. Colour
   and shape are lib/ui.css's business.
   ───────────────────────────────────────────────────────────────────────── */

/* WIDE — a shopfront. For landings, galleries, portfolios.
   The point is to make the screen feel roomy: content runs close to the
   edges, there are many columns, and there is no narrow middle column
   leaving a wide screen wasted. */
.shell-wide {
  width: 100%;
  max-width: var(--w-wide);
  margin-inline: auto;
  padding-inline: max(var(--gutter), var(--safe-l), var(--safe-r));
}

/* DASHBOARD — still wide, but the content is data. Density up, decoration
   down. */
.shell-dash {
  width: 100%;
  max-width: var(--w-dash);
  margin-inline: auto;
  padding-inline: max(var(--gutter), var(--safe-l), var(--safe-r));
}

/* COMPACT — a small app. One column in the middle, about phone width, so
   that even on a laptop it feels like an app being held rather than a web
   page stretched out. */
.shell-compact {
  width: 100%;
  max-width: var(--w-compact);
  margin-inline: auto;
  padding-inline: max(var(--s4), var(--safe-l), var(--safe-r));
}

/* Grid — the columns adjust themselves, so no manual breakpoints. */
.grid { display: grid; gap: var(--s4); }
.grid-2 { grid-template-columns: repeat(auto-fit, minmax(min(100%, 320px), 1fr)); }
.grid-3 { grid-template-columns: repeat(auto-fit, minmax(min(100%, 280px), 1fr)); }
.grid-4 { grid-template-columns: repeat(auto-fit, minmax(min(100%, 220px), 1fr)); }
.grid-5 { grid-template-columns: repeat(auto-fit, minmax(min(100%, 172px), 1fr)); }
.grid-tight { gap: var(--s2); }
.grid-lg { gap: var(--s5); }
/* A grid whose column floor you set yourself: style="--min: 340px" */
.grid-auto { grid-template-columns: repeat(auto-fit, minmax(min(100%, var(--min, 260px)), 1fr)); }
/* A cell asking for two columns — it gives way to one if only one fits. */
.span-2 { grid-column: span 2; }
@media (max-width: 640px) { .span-2 { grid-column: auto; } }

/* A horizontal row that is safe on a narrow screen */
.row { display: flex; align-items: center; gap: var(--s3); flex-wrap: wrap; }
.row-tight { gap: var(--s2); }
.row-nowrap { flex-wrap: nowrap; min-width: 0; }
.row-top { align-items: flex-start; }
.row-between { justify-content: space-between; }
.row-end { justify-content: flex-end; }
.spacer { flex: 1 1 auto; }

/* Space between sections — one ladder, do not invent a new one per page.
   The values already shrink on a phone through --s7/--s9. */
.stack > * + * { margin-top: var(--s4); }
/* A tight stack uses grid, not margins: its contents are often inline <b>
   and <span>, and margin-top will not separate those. Its children fill the
   width by default — a .progress or .bars track inside it must not shrink to
   nothing — and only the things that are markers in their own right (chips,
   badges, buttons) keep to their natural size. */
.stack-tight { display: grid; gap: var(--s1); justify-items: stretch; }
.stack-tight > :is(.chip, .badge, .btn, .kbd, .trend, .saved, .avatar, .dot, .meta, .eyebrow) { justify-self: start; }
.stack-lg > * + * { margin-top: var(--s7); }
.section { padding-block: var(--s9); }
.section-sm { padding-block: var(--s7); }
.center { display: grid; place-items: center; }

/* ── Two-column dashboard ─────────────────────────────────────────────────
   A thin left side, the main content stretching. Below 900px the sidebar is
   not thrown away — it becomes a row of links that scrolls sideways, so
   someone on a phone can still jump between sections without scrolling
   past a long list. */
.split {
  display: grid;
  grid-template-columns: minmax(0, var(--side-w, 260px)) minmax(0, 1fr);
  gap: var(--s6);
  align-items: start;
}
.split-r { grid-template-columns: minmax(0, 1fr) minmax(0, var(--side-w, 300px)); }

@media (max-width: 900px) {
  .split, .split-r { grid-template-columns: minmax(0, 1fr); gap: var(--s5); }
  .split-r > .side { order: 2; }

  /* A sidebar with little in it stays a horizontal row. One with a search box
     (.side[data-index]) does not — a list that long will never be pleasant
     to scroll sideways; it moves into a bottom sheet, handled by lib/contents.js. */
  .side:not([data-index]) {
    position: sticky;
    top: calc(var(--h-topbar) + var(--safe-t));
    z-index: var(--z-side);
    display: flex;
    gap: var(--s1);
    overflow-x: auto;
    overscroll-behavior-x: contain;
    scrollbar-width: none;
    padding-block: var(--s2);
    background: color-mix(in srgb, var(--paper) 92%, transparent);
    backdrop-filter: saturate(180%) blur(12px);
    border-bottom: 1px solid var(--line);
  }
  .side:not([data-index])::-webkit-scrollbar { display: none; }
  .side:not([data-index]) a { white-space: nowrap; flex: none; }
  /* Group headings are useless in a horizontal row — they only take space. */
  .side:not([data-index]) .group { display: none; }

  /* Table of contents: hidden from the page flow, reappearing in a bottom
     sheet. The same elements, moved — not duplicated. */
  .split > .side[data-index] { display: none; }
  .sheet-body > .side[data-index] {
    display: flex; position: static; max-height: min(70dvh, 560px);
  }

  /* Unless the list is INSIDE the drawer: there vertical room is plentiful,
     so it goes back to being a full upright list with its group headings.
     That horizontal shape was only for a sidebar stuck under the top bar of
     an ordinary page. */
  .appside .side {
    display: grid; position: static; overflow: visible;
    background: none; backdrop-filter: none; border-bottom: 0;
    padding-block: 0;
  }
  .appside .side a { white-space: normal; }
  .appside .side .group { display: block; }
}

/* ── App frame ────────────────────────────────────────────────────────────
   Top bar + fixed sidebar + content that scrolls on its own. This is the
   shape for an app that is genuinely worked in, not a page read once.
   Below 900px the sidebar becomes a drawer that covers the screen
   (off-canvas) — opened with a data-drawer button, closed with Esc, a
   backdrop click, or a swipe. The same elements, nothing duplicated. */
.appshell {
  display: grid;
  grid-template-columns: minmax(0, var(--side-w, 264px)) minmax(0, 1fr);
  min-height: 100dvh;
}
.appside {
  border-right: 1px solid var(--line);
  background: var(--paper-2);
  padding: var(--s4);
  padding-left: max(var(--s4), var(--safe-l));
  display: flex;
  flex-direction: column;
  gap: var(--s2);
  position: sticky;
  top: 0;
  height: 100dvh;
  overflow-y: auto;
  overscroll-behavior: contain;
}
.appmain {
  min-width: 0;
  display: flex;
  flex-direction: column;
  padding-bottom: var(--safe-b);
}
.appbody { flex: 1; padding: var(--s5) max(var(--s5), var(--safe-r)) var(--s7) var(--s5); min-width: 0; }

@media (max-width: 900px) {
  .appshell { grid-template-columns: minmax(0, 1fr); }
  .appside {
    position: fixed;
    inset: 0 auto 0 0;
    z-index: var(--z-overlay);
    width: min(300px, 84vw);
    transform: translateX(-101%);
    transition: transform var(--d-mid) var(--ease);
    box-shadow: var(--shadow-2);
    padding-top: max(var(--s4), var(--safe-t));
    padding-bottom: max(var(--s4), var(--safe-b));
  }
  .appside.is-on { transform: none; }
  .appbody { padding: var(--s4) max(var(--s4), var(--safe-r)) var(--s7) max(var(--s4), var(--safe-l)); }
}
@media (prefers-reduced-motion: reduce) {
  .appside { transition: none; }
}

/* The dark backdrop behind the drawer. One element, reused. */
.scrim {
  position: fixed; inset: 0; z-index: calc(var(--z-overlay) - 1);
  background: rgb(0 0 0 / .45);
  opacity: 0; pointer-events: none;
  transition: opacity var(--d-mid) var(--ease);
}
.scrim.is-on { opacity: 1; pointer-events: auto; }

/* ── Page head ────────────────────────────────────────────────────────────
   Title left, buttons right; on a phone the buttons drop below and go full
   width so a thumb can reach them. */
.pagehead {
  display: flex; align-items: flex-end; justify-content: space-between;
  gap: var(--s4); flex-wrap: wrap;
  padding-bottom: var(--s4); border-bottom: 1px solid var(--line);
}
.pagehead .titles { min-width: 0; display: grid; gap: var(--s1); }
.pagehead .acts { display: flex; gap: var(--s2); flex-wrap: wrap; }
@media (max-width: 640px) {
  .pagehead .acts { width: 100%; }
  .pagehead .acts > .btn { flex: 1 1 auto; }
}

/* ── List + detail ────────────────────────────────────────────────────────
   The two-panel pattern (inbox, files, messages). On a phone only one is
   visible: add .show-detail once something is selected. */
.listdetail { display: grid; grid-template-columns: minmax(0, var(--list-w, 320px)) minmax(0, 1fr); gap: var(--s5); align-items: start; }
@media (max-width: 900px) {
  .listdetail { grid-template-columns: minmax(0, 1fr); }
  .listdetail > .detail { display: none; }
  .listdetail.show-detail > .list { display: none; }
  .listdetail.show-detail > .detail { display: block; }
}

/* ── Show/hide helpers ────────────────────────────────────────────────────
   Deliberately only three pairs, locked to the system's breakpoint ladder.
   If you need a fourth, the layout is most likely what is wrong. */
@media (max-width: 640px)  { .hide-sm { display: none !important; } }
@media (min-width: 641px)  { .only-sm { display: none !important; } }
@media (max-width: 900px)  { .hide-md { display: none !important; } }
@media (min-width: 901px)  { .only-md { display: none !important; } }
/* Only for tools that need a real keyboard or mouse (e.g. the ⌘K palette) */
@media (hover: none) { .only-mouse { display: none !important; } }

/* Long text stops before the eye tires of finding the next line */
.measure { max-width: var(--w-prose); }

