@layer foundation, themes, base, primitives, components, modules, patterns;

/* ============================ @layer foundation */

/* --- styles/foundation/tokens.css */
@layer foundation {
/* Foundation tokens: imloka Design System v2.1, Appendix A.
 *
 * Scale values only. No colour lives here; colour is the `themes` layer's
 * exclusive property (build constraint C2, enforced by tools/lint-css.mjs).
 * These values are transcribed from the spec, not invented. */

:root {
  /* --- spacing (§Spacing) ------------------------------------------------ */
  --space-1: 4px;
  --space-2: 8px;
  --space-3: 12px;
  --space-4: 16px;
  --space-6: 24px;
  --space-8: 32px;
  --space-10: 40px;
  --space-12: 48px;
  --space-16: 64px;
  --space-20: 80px;
  --space-24: 96px;

  /* --- radius (§Geometry) ------------------------------------------------
   * Pill geometry is semantic, not decorative: chips and toggles only. */
  --radius-none: 0;
  --radius-xs: 2px;
  --radius-sm: 4px;
  --radius-layer: 8px;
  --radius-pill: 999px;

  /* --- widths (§Global Page Shell) --------------------------------------- */
  --content-max: 90rem;   /* ~1440px working width */
  --reading-max: 45rem;   /* ~720px prose measure  */
  --rail-w: 15rem;        /* ~240px: the article's Contents rail, and the
                             matching empty rail that balances it */

  /* --- outer gutters, per the viewport table ----------------------------- */
  --gutter: 16px;

  /* --- editorial grid ---------------------------------------------------- */
  --grid-columns: 2;
  --grid-gap: 8px;

  /* --- motion (§Motion) --------------------------------------------------
   * `transition: all` is prohibited (C10). Name properties explicitly. */
  /* Near-zero, for the reduced-motion override. Not 0: a 0s transition can
   * skip transitionend handlers that carry semantic state. */
  --motion-none: 0.01ms;
  --motion-instant: 80ms;
  --motion-fast: 140ms;
  --motion-base: 180ms;
  --motion-medium: 240ms;
  --motion-large: 320ms;
  --motion-slow: 420ms;

  /* Long-running indicator loops. The spec gives ranges rather than points:
   * spinner ~700-900ms per revolution, skeleton pulse ~1200-1800ms. */
  --motion-spinner: 800ms;
  --motion-skeleton: 1500ms;
  /* Reduced-motion spinner: slow enough to read as calm, still moving enough
   * to read as indeterminate. Stopping it entirely would lose that meaning. */
  --motion-spinner-reduced: 3000ms;

  --ease-standard: cubic-bezier(.2, 0, 0, 1);
  --ease-enter: cubic-bezier(.16, 1, .3, 1);
  --ease-exit: cubic-bezier(.4, 0, 1, 1);
  --ease-linear: linear;

  /* --- semantic z-layers (§Semantic Z-Layers) ----------------------------
   * Never escalate to arbitrary z-index values. */
  --z-base: 0;
  --z-raised: 10;
  --z-header: 100;
  --z-menu: 200;
  --z-search: 200;
  --z-popover: 300;
  --z-toast: 400;
  --z-modal: 500;

  /* --- header heights (§Header Heights) ---------------------------------- */
  --header-h: 60px;
  --header-h-compact: 60px;

  /* --- minimum interactive target (§Touch) ------------------------------- */
  --target-min: 44px;

  /* --- type roles: Latin mobile defaults; desktop set in base/type.css ---- */
  --type-display-xl: 40px;   --type-display-xl-lh: 1.06;
  --type-display-l: 34px;    --type-display-l-lh: 1.08;
  --type-display-m: 28px;    --type-display-m-lh: 1.12;
  --type-heading-l: 24px;    --type-heading-l-lh: 1.2;
  --type-heading-m: 20px;    --type-heading-m-lh: 1.28;
  --type-heading-s: 17px;    --type-heading-s-lh: 1.34;
  --type-body-l: 18px;       --type-body-l-lh: 1.6;
  --type-body: 16px;         --type-body-lh: 1.65;
  --type-ui: 15px;           --type-ui-lh: 1.47;
  --type-meta: 13px;         --type-meta-lh: 1.38;
  --type-label: 11px;        --type-label-lh: 1.34;

  --weight-regular: 400;
  --weight-medium: 500;
  --weight-semibold: 600;
  --weight-bold: 700;

  /* Latin display tracking. Devanagari overrides this to `normal` (C8). */
  --track-display: -0.02em;
  --track-heading: -0.01em;
  --track-label: 0.07em;
}

/* Gutters and grid widen with the viewport (§Global Page Shell table). */
@media (min-width: 768px) {
  :root { --gutter: 24px; --grid-columns: 6; --grid-gap: 10px; --header-h: 64px; }
}
@media (min-width: 1024px) {
  :root { --gutter: 24px; --grid-columns: 12; --grid-gap: 12px; --header-h: 76px; }
}
@media (min-width: 1200px) { :root { --gutter: 32px; } }
@media (min-width: 1440px) { :root { --gutter: 44px; } }
}

/* ============================ @layer themes */

/* --- styles/themes/palettes.css */
@layer themes {
/* Theme palettes: imloka Design System v2.1, §Exact Color System.
 *
 * GENERATED by tools/gen-themes.py. Do not edit by hand; edit the tables there.
 *
 * This is the ONLY file permitted to contain literal colour values (build
 * constraint C2). Every other layer consumes the semantic tokens defined here,
 * so a component never branches on theme name.
 *
 * Theme (personality) and Appearance (luminance) are independent. The pre-paint
 * script in the document head resolves `system` to light or dark and stamps
 * data-resolved-appearance before the first paint, so there is no flash.
 *
 *   Theme      = ocean | forest | sunset | violet | graphite
 *   Appearance = system | light | dark   → resolved to light | dark
 *
 * Geometry is identical across every theme. Themes change atmosphere, not
 * hierarchy. */


/* ---------------------------------------------------------- ocean */

[data-theme="ocean"][data-resolved-appearance="light"] {
  color-scheme: light;
  --page-bg: #F5F9FB;
  --surface: #FFFFFF;
  --surface-alt: #EAF3F7;
  --text: #142129;
  --text-muted: #5C6B73;
  --primary: #0B6E99;
  --primary-strong: #075675;
  --secondary: #D9ECF4;
  --accent: #168DB9;
  --text-on-primary: #FFFFFF;
  --border: #C9D9E0;
  --focus: #0B6E99;
  --image-overlay: rgba(5,33,48,.52);

  /* derived roles: components consume these, never a raw hex */
  --link: var(--primary);
  --link-hover: var(--primary-strong);
  --selection-bg: var(--secondary);
  --selection-text: var(--text);
  --commercial-surface: var(--surface-alt);
  --primary-surface: color-mix(in srgb, var(--primary) 14%, transparent);
  --primary-glow: color-mix(in srgb, var(--primary) 55%, transparent);

  /* Text sitting on top of --image-overlay. Every overlay value in the
   * system is dark in both appearances, so this stays white across all
   * ten combinations; but it is a named role rather than a literal,
   * so a future edition treatment can change it in one place. */
  --text-on-image: #FFFFFF;

  --success: #24613F;
  --success-surface: #E7F2EA;
  --warning: #7A4B00;
  --warning-surface: #FBF0D8;
  --danger: #9B2F2B;
  --danger-surface: #FBE8E6;
}

[data-theme="ocean"][data-resolved-appearance="dark"] {
  color-scheme: dark;
  --page-bg: #0D151A;
  --surface: #121D23;
  --surface-alt: #18272F;
  --text: #EAF2F5;
  --text-muted: #A7B5BC;
  --primary: #3A9CC5;
  --primary-strong: #61B6D8;
  --secondary: #173744;
  --accent: #69C7EA;
  --text-on-primary: #07161D;
  --border: #2A3C45;
  --focus: #7AD5F5;
  --image-overlay: rgba(0,9,14,.48);

  /* derived roles: components consume these, never a raw hex */
  --link: var(--primary);
  --link-hover: var(--primary-strong);
  --selection-bg: var(--secondary);
  --selection-text: var(--text);
  --commercial-surface: var(--surface-alt);
  --primary-surface: color-mix(in srgb, var(--primary) 14%, transparent);
  --primary-glow: color-mix(in srgb, var(--primary) 55%, transparent);

  /* Text sitting on top of --image-overlay. Every overlay value in the
   * system is dark in both appearances, so this stays white across all
   * ten combinations; but it is a named role rather than a literal,
   * so a future edition treatment can change it in one place. */
  --text-on-image: #FFFFFF;

  --success: #8CC9A1;
  --success-surface: #1B2F22;
  --warning: #E5B758;
  --warning-surface: #332813;
  --danger: #F0958F;
  --danger-surface: #3A1D1B;
}


/* ---------------------------------------------------------- forest */

[data-theme="forest"][data-resolved-appearance="light"] {
  color-scheme: light;
  --page-bg: #F6F8F5;
  --surface: #FFFFFF;
  --surface-alt: #ECF2ED;
  --text: #18211B;
  --text-muted: #5C685F;
  --primary: #2F6B4F;
  --primary-strong: #24533D;
  --secondary: #DFEAE2;
  --accent: #4F8A68;
  --text-on-primary: #FFFFFF;
  --border: #CCD8CF;
  --focus: #2F6B4F;
  --image-overlay: rgba(13,36,25,.52);

  /* derived roles: components consume these, never a raw hex */
  --link: var(--primary);
  --link-hover: var(--primary-strong);
  --selection-bg: var(--secondary);
  --selection-text: var(--text);
  --commercial-surface: var(--surface-alt);
  --primary-surface: color-mix(in srgb, var(--primary) 14%, transparent);
  --primary-glow: color-mix(in srgb, var(--primary) 55%, transparent);

  /* Text sitting on top of --image-overlay. Every overlay value in the
   * system is dark in both appearances, so this stays white across all
   * ten combinations; but it is a named role rather than a literal,
   * so a future edition treatment can change it in one place. */
  --text-on-image: #FFFFFF;

  --success: #24613F;
  --success-surface: #E7F2EA;
  --warning: #7A4B00;
  --warning-surface: #FBF0D8;
  --danger: #9B2F2B;
  --danger-surface: #FBE8E6;
}

[data-theme="forest"][data-resolved-appearance="dark"] {
  color-scheme: dark;
  --page-bg: #101611;
  --surface: #151E17;
  --surface-alt: #1C2920;
  --text: #EDF3EE;
  --text-muted: #ABB8AE;
  --primary: #5A9E75;
  --primary-strong: #78B78E;
  --secondary: #22372A;
  --accent: #8BC9A1;
  --text-on-primary: #08150D;
  --border: #304437;
  --focus: #9AD9AF;
  --image-overlay: rgba(5,18,10,.48);

  /* derived roles: components consume these, never a raw hex */
  --link: var(--primary);
  --link-hover: var(--primary-strong);
  --selection-bg: var(--secondary);
  --selection-text: var(--text);
  --commercial-surface: var(--surface-alt);
  --primary-surface: color-mix(in srgb, var(--primary) 14%, transparent);
  --primary-glow: color-mix(in srgb, var(--primary) 55%, transparent);

  /* Text sitting on top of --image-overlay. Every overlay value in the
   * system is dark in both appearances, so this stays white across all
   * ten combinations; but it is a named role rather than a literal,
   * so a future edition treatment can change it in one place. */
  --text-on-image: #FFFFFF;

  --success: #8CC9A1;
  --success-surface: #1B2F22;
  --warning: #E5B758;
  --warning-surface: #332813;
  --danger: #F0958F;
  --danger-surface: #3A1D1B;
}


/* ---------------------------------------------------------- sunset */

[data-theme="sunset"][data-resolved-appearance="light"] {
  color-scheme: light;
  --page-bg: #FBF7F4;
  --surface: #FFFFFF;
  --surface-alt: #F6EAE4;
  --text: #251B18;
  --text-muted: #70615B;
  --primary: #A7441F;
  --primary-strong: #843619;
  --secondary: #F3DED4;
  --accent: #C96A38;
  --text-on-primary: #FFFFFF;
  --border: #E1CEC5;
  --focus: #A7441F;
  --image-overlay: rgba(51,23,12,.52);

  /* derived roles: components consume these, never a raw hex */
  --link: var(--primary);
  --link-hover: var(--primary-strong);
  --selection-bg: var(--secondary);
  --selection-text: var(--text);
  --commercial-surface: var(--surface-alt);
  --primary-surface: color-mix(in srgb, var(--primary) 14%, transparent);
  --primary-glow: color-mix(in srgb, var(--primary) 55%, transparent);

  /* Text sitting on top of --image-overlay. Every overlay value in the
   * system is dark in both appearances, so this stays white across all
   * ten combinations; but it is a named role rather than a literal,
   * so a future edition treatment can change it in one place. */
  --text-on-image: #FFFFFF;

  --success: #24613F;
  --success-surface: #E7F2EA;
  --warning: #7A4B00;
  --warning-surface: #FBF0D8;
  --danger: #9B2F2B;
  --danger-surface: #FBE8E6;
}

[data-theme="sunset"][data-resolved-appearance="dark"] {
  color-scheme: dark;
  --page-bg: #18110F;
  --surface: #211713;
  --surface-alt: #2C1E18;
  --text: #F6EEEA;
  --text-muted: #C2AEA5;
  --primary: #CF7247;
  --primary-strong: #E08A61;
  --secondary: #41271D;
  --accent: #F09A70;
  --text-on-primary: #1B0D07;
  --border: #503228;
  --focus: #FFAC82;
  --image-overlay: rgba(27,9,4,.48);

  /* derived roles: components consume these, never a raw hex */
  --link: var(--primary);
  --link-hover: var(--primary-strong);
  --selection-bg: var(--secondary);
  --selection-text: var(--text);
  --commercial-surface: var(--surface-alt);
  --primary-surface: color-mix(in srgb, var(--primary) 14%, transparent);
  --primary-glow: color-mix(in srgb, var(--primary) 55%, transparent);

  /* Text sitting on top of --image-overlay. Every overlay value in the
   * system is dark in both appearances, so this stays white across all
   * ten combinations; but it is a named role rather than a literal,
   * so a future edition treatment can change it in one place. */
  --text-on-image: #FFFFFF;

  --success: #8CC9A1;
  --success-surface: #1B2F22;
  --warning: #E5B758;
  --warning-surface: #332813;
  --danger: #F0958F;
  --danger-surface: #3A1D1B;
}


/* ---------------------------------------------------------- violet */

[data-theme="violet"][data-resolved-appearance="light"] {
  color-scheme: light;
  --page-bg: #F8F6FB;
  --surface: #FFFFFF;
  --surface-alt: #F0EBF8;
  --text: #201B29;
  --text-muted: #655D70;
  --primary: #6B4FB3;
  --primary-strong: #543B93;
  --secondary: #E7DEF4;
  --accent: #8969D0;
  --text-on-primary: #FFFFFF;
  --border: #D6CBE5;
  --focus: #6B4FB3;
  --image-overlay: rgba(28,18,45,.52);

  /* derived roles: components consume these, never a raw hex */
  --link: var(--primary);
  --link-hover: var(--primary-strong);
  --selection-bg: var(--secondary);
  --selection-text: var(--text);
  --commercial-surface: var(--surface-alt);
  --primary-surface: color-mix(in srgb, var(--primary) 14%, transparent);
  --primary-glow: color-mix(in srgb, var(--primary) 55%, transparent);

  /* Text sitting on top of --image-overlay. Every overlay value in the
   * system is dark in both appearances, so this stays white across all
   * ten combinations; but it is a named role rather than a literal,
   * so a future edition treatment can change it in one place. */
  --text-on-image: #FFFFFF;

  --success: #24613F;
  --success-surface: #E7F2EA;
  --warning: #7A4B00;
  --warning-surface: #FBF0D8;
  --danger: #9B2F2B;
  --danger-surface: #FBE8E6;
}

[data-theme="violet"][data-resolved-appearance="dark"] {
  color-scheme: dark;
  --page-bg: #15111B;
  --surface: #1D1726;
  --surface-alt: #271F33;
  --text: #F2EDF7;
  --text-muted: #B7ABBF;
  --primary: #8D70CF;
  --primary-strong: #A58DDA;
  --secondary: #35284A;
  --accent: #B39BE5;
  --text-on-primary: #150D22;
  --border: #453655;
  --focus: #C0AAF0;
  --image-overlay: rgba(15,8,25,.48);

  /* derived roles: components consume these, never a raw hex */
  --link: var(--primary);
  --link-hover: var(--primary-strong);
  --selection-bg: var(--secondary);
  --selection-text: var(--text);
  --commercial-surface: var(--surface-alt);
  --primary-surface: color-mix(in srgb, var(--primary) 14%, transparent);
  --primary-glow: color-mix(in srgb, var(--primary) 55%, transparent);

  /* Text sitting on top of --image-overlay. Every overlay value in the
   * system is dark in both appearances, so this stays white across all
   * ten combinations; but it is a named role rather than a literal,
   * so a future edition treatment can change it in one place. */
  --text-on-image: #FFFFFF;

  --success: #8CC9A1;
  --success-surface: #1B2F22;
  --warning: #E5B758;
  --warning-surface: #332813;
  --danger: #F0958F;
  --danger-surface: #3A1D1B;
}


/* ---------------------------------------------------------- graphite */

[data-theme="graphite"][data-resolved-appearance="light"] {
  color-scheme: light;
  --page-bg: #F6F7F8;
  --surface: #FFFFFF;
  --surface-alt: #ECEFF1;
  --text: #181B1F;
  --text-muted: #60666D;
  --primary: #3E4651;
  --primary-strong: #2D333C;
  --secondary: #E2E6E9;
  --accent: #66717D;
  --text-on-primary: #FFFFFF;
  --border: #D1D6DA;
  --focus: #3E4651;
  --image-overlay: rgba(15,18,22,.52);

  /* derived roles: components consume these, never a raw hex */
  --link: var(--primary);
  --link-hover: var(--primary-strong);
  --selection-bg: var(--secondary);
  --selection-text: var(--text);
  --commercial-surface: var(--surface-alt);
  --primary-surface: color-mix(in srgb, var(--primary) 14%, transparent);
  --primary-glow: color-mix(in srgb, var(--primary) 55%, transparent);

  /* Text sitting on top of --image-overlay. Every overlay value in the
   * system is dark in both appearances, so this stays white across all
   * ten combinations; but it is a named role rather than a literal,
   * so a future edition treatment can change it in one place. */
  --text-on-image: #FFFFFF;

  --success: #24613F;
  --success-surface: #E7F2EA;
  --warning: #7A4B00;
  --warning-surface: #FBF0D8;
  --danger: #9B2F2B;
  --danger-surface: #FBE8E6;
}

[data-theme="graphite"][data-resolved-appearance="dark"] {
  color-scheme: dark;
  --page-bg: #111315;
  --surface: #171A1D;
  --surface-alt: #202428;
  --text: #F0F2F4;
  --text-muted: #ADB3B9;
  --primary: #7F8A96;
  --primary-strong: #9BA5AE;
  --secondary: #2B3035;
  --accent: #B3BDC6;
  --text-on-primary: #101214;
  --border: #353B41;
  --focus: #C5CED6;
  --image-overlay: rgba(0,0,0,.48);

  /* derived roles: components consume these, never a raw hex */
  --link: var(--primary);
  --link-hover: var(--primary-strong);
  --selection-bg: var(--secondary);
  --selection-text: var(--text);
  --commercial-surface: var(--surface-alt);
  --primary-surface: color-mix(in srgb, var(--primary) 14%, transparent);
  --primary-glow: color-mix(in srgb, var(--primary) 55%, transparent);

  /* Text sitting on top of --image-overlay. Every overlay value in the
   * system is dark in both appearances, so this stays white across all
   * ten combinations; but it is a named role rather than a literal,
   * so a future edition treatment can change it in one place. */
  --text-on-image: #FFFFFF;

  --success: #8CC9A1;
  --success-surface: #1B2F22;
  --warning: #E5B758;
  --warning-surface: #332813;
  --danger: #F0958F;
  --danger-surface: #3A1D1B;
}
}

/* ============================ @layer base */

/* --- styles/base/reset.css */
@layer base {
/* Base: reset, document ground, focus, selection.
 * Consumes semantic tokens only. */

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

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

body {
  margin: 0;
  background: var(--page-bg);
  color: var(--text);
  font-size: var(--type-body);
  line-height: var(--type-body-lh);
  font-weight: var(--weight-regular);
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
}

h1, h2, h3, h4, h5, h6, p, figure, blockquote, dl, dd { margin: 0; }

/* [hidden] must win. Any component that sets `display` (and most flex/grid
   ones do) silently defeats the attribute otherwise, leaving controls visible
   that JS believes it has hidden. */
[hidden] { display: none !important; }
ul[class], ol[class] { list-style: none; margin: 0; padding: 0; }

img, picture, svg, video { display: block; max-width: 100%; }
img { height: auto; }

input, button, textarea, select { font: inherit; color: inherit; }

a { color: var(--link); }
a:hover { color: var(--link-hover); }

::selection {
  background: var(--selection-bg);
  color: var(--selection-text);
}

/* §Focus: 2px, offset, using --focus so it stays visible in all ten
 * theme/appearance combinations. Never remove without replacing. */
:focus-visible {
  outline: 2px solid var(--focus);
  outline-offset: 2px;
  border-radius: var(--radius-xs);
}
:focus:not(:focus-visible) { outline: none; }

/* §Landmarks: skip link is the first focusable element on every normal route. */
.imloka-skip-link {
  position: absolute;
  left: var(--space-4);
  top: var(--space-4);
  z-index: var(--z-modal);
  padding: var(--space-3) var(--space-4);
  background: var(--surface);
  color: var(--text);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  font-size: var(--type-ui);
  font-weight: var(--weight-medium);
  text-decoration: none;
  transform: translateY(calc(-100% - var(--space-8)));
  transition: transform var(--motion-fast) var(--ease-standard);
}
.imloka-skip-link:focus { transform: translateY(0); }

/* Shared page shell (§Global Page Shell and Responsive Grid). */
.imloka-shell {
  width: 100%;
  max-width: var(--content-max);
  margin-inline: auto;
  padding-inline: var(--gutter);
}

/* §Reduced Motion: simplify spatial travel, preserve every semantic state
 * change. This is a simplification, not a removal. */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: var(--motion-none) !important;
    animation-iteration-count: 1 !important;
    transition-duration: var(--motion-none) !important;
    scroll-behavior: auto !important;
  }

  /* A blanket rule is the right default but the wrong absolute: an
   * indeterminate spinner that stops moving stops meaning "working". The spec
   * asks reduced motion to simplify spatial and decorative movement while
   * PRESERVING semantic feedback, so motion that carries state opts back in:
   * slower and calmer, but still alive. */
  [data-motion="essential"],
  [data-motion="essential"]::before,
  [data-motion="essential"]::after {
    animation-duration: var(--motion-spinner-reduced) !important;
    animation-iteration-count: infinite !important;
  }
}

/* §Color and High Contrast: forced-colors must not lose structure. */
@media (forced-colors: active) {
  .imloka-skip-link { border: 1px solid CanvasText; }
  :focus-visible { outline-color: Highlight; }
}
}

/* --- styles/base/type.css */
@layer base {
/* Base: typography. imloka Design System v2.1, §Typography System.
 *
 * "One editorial hierarchy, expressed naturally in each script."
 *
 * Semantic roles are shared between English and Hindi; the metrics behind them
 * differ. Inter leads both stacks so Latin glyphs, numerals, URLs and acronyms
 * keep the Inter system on Hindi pages while the bundled Devanagari face
 * ("imloka Devanagari Fallback": a self-hosted Noto Sans Devanagari, Hind
 * as its self-hosted secondary) supplies Devanagari. */

:root {
  --font-latin:
    "InterVariable", "Inter", "Helvetica Neue", Helvetica, Arial, sans-serif;

  /* Order matters, and every entry earns its place:
   *   Inter        first, so Latin glyphs, numerals, URLs and acronyms keep the
   *                Inter system on a Hindi page while the entries below supply
   *                Devanagari
   *   imloka …     the PRIMARY Devanagari face: self-hosted, so it loads
   *                deterministically for every reader regardless of OS. This
   *                is a Noto Sans Devanagari file, but declared under a unique
   *                family name rather than "Noto Sans Devanagari": using the
   *                real name would make it indistinguishable from whatever a
   *                reader's OS happens to have locally installed under that
   *                same name, which may be a different version with different
   *                metrics. Naming it uniquely guarantees this exact asset is
   *                what renders, not a look-alike.
   *   Hind         SECONDARY: self-hosted, reached only if the primary file
   *                above genuinely fails to load (network failure, blocked
   *                request). Metric-compatible, so the fallback is a quiet one.
   *   Noto …       named system Devanagari: reached only if both self-hosted
   *                faces above have failed
   *   Kohinoor /   macOS and iOS system Devanagari. Previously missing: the
   *   Devanagari   stack named two Windows faces and no Apple one, so an Apple
   *   Sangam MN    reader fell straight through to generic sans-serif
   *   Nirmala UI   Windows 8+
   *   Mangal       older Windows
   *   sans-serif   only reached if every named face above has failed */
  --font-multiscript:
    "InterVariable", "Inter", "imloka Devanagari Fallback", "Hind",
    "Noto Sans Devanagari", "Kohinoor Devanagari", "Devanagari Sangam MN",
    "Nirmala UI", "Mangal", sans-serif;
}

html[lang="en"] { font-family: var(--font-latin); }
html[lang="hi"] { font-family: var(--font-multiscript); }

/* The rules above bind the family to the DOCUMENT language, which is right for
 * the page and wrong for the few Devanagari strings that appear inside an
 * English one: the language switcher's हिंदी, and an article's "हिंदी में पढ़ें"
 * link. Those are exactly the two controls a Hindi reader uses to find their
 * edition; and they were inheriting the Latin stack, which has no Devanagari
 * coverage, so they fell through to whatever the OS resolves `sans-serif` to.
 *
 * Naming the multiscript stack here costs no bytes. Its webfont families are
 * simply not declared on an English route, so the browser skips them and
 * reaches the system Devanagari names the stack lists: Nirmala UI, Mangal,
 * Kohinoor Devanagari, Devanagari Sangam MN. That is the point: an intentional
 * fallback chain rather than a generic one. */
:lang(hi) { font-family: var(--font-multiscript); }

/* --- desktop metrics (§English / Latin Desktop Metrics) ------------------- */
@media (min-width: 1024px) {
  :root {
    --type-display-xl: 64px;  --type-display-xl-lh: 1.06;
    --type-display-l: 48px;   --type-display-l-lh: 1.08;
    --type-display-m: 36px;   --type-display-m-lh: 1.11;
    --type-heading-l: 28px;   --type-heading-l-lh: 1.21;
    --type-heading-m: 22px;   --type-heading-m-lh: 1.27;
    --type-heading-s: 18px;   --type-heading-s-lh: 1.33;
    --type-body-l: 20px;      --type-body-l-lh: 1.6;
    --type-body: 17px;        --type-body-lh: 1.65;
    --type-ui: 15px;          --type-ui-lh: 1.47;
    --type-meta: 13px;        --type-meta-lh: 1.38;
    --type-label: 12px;       --type-label-lh: 1.33;
  }
}

/* --- Devanagari metrics (§Hindi / Devanagari Desktop Metrics) -------------
 * The semantic hierarchy is locked; the leading is not. Devanagari needs more
 * vertical space for shaping: compressing it damages marks and the shirorekha.
 * These are production starting points pending native-reader proofing. */
html[lang="hi"] {
  --type-display-xl-lh: 1.19;
  --type-display-l-lh: 1.21;
  --type-display-m-lh: 1.22;
  --type-heading-l-lh: 1.36;
  --type-heading-m-lh: 1.45;
  --type-heading-s-lh: 1.53;
  --type-body-l-lh: 1.7;
  --type-body-lh: 1.76;
  --type-ui-lh: 1.6;
  --type-meta-lh: 1.54;
  --type-label-lh: 1.5;
}

/* Devanagari tracking rule (C8). Latin's negative headline tracking is wrong
 * for Devanagari, and Label must never fake uppercase. */
:lang(hi) {
  letter-spacing: normal;
}

/* --- semantic type roles -------------------------------------------------- */
.imloka-display-xl,
.imloka-display-l,
.imloka-display-m {
  font-weight: var(--weight-semibold);
  letter-spacing: var(--track-display);
}

.imloka-heading-l,
.imloka-heading-m,
.imloka-heading-s {
  font-weight: var(--weight-semibold);
  letter-spacing: var(--track-heading);
}

.imloka-display-xl { font-size: var(--type-display-xl); line-height: var(--type-display-xl-lh); }
.imloka-display-l  { font-size: var(--type-display-l);  line-height: var(--type-display-l-lh); }
.imloka-display-m  { font-size: var(--type-display-m);  line-height: var(--type-display-m-lh); }
.imloka-heading-l  { font-size: var(--type-heading-l);  line-height: var(--type-heading-l-lh); }
.imloka-heading-m  { font-size: var(--type-heading-m);  line-height: var(--type-heading-m-lh); }
.imloka-heading-s  { font-size: var(--type-heading-s);  line-height: var(--type-heading-s-lh); }

.imloka-body-l { font-size: var(--type-body-l); line-height: var(--type-body-l-lh); }
.imloka-body   { font-size: var(--type-body);   line-height: var(--type-body-lh); }

.imloka-ui {
  font-size: var(--type-ui);
  line-height: var(--type-ui-lh);
  font-weight: var(--weight-medium);
}

.imloka-meta {
  font-size: var(--type-meta);
  line-height: var(--type-meta-lh);
  color: var(--text-muted);
}

/* Label is a semantic role, not an uppercase rule. Latin may set it in caps
 * with positive tracking; Devanagari uses natural text at normal tracking. */
.imloka-label {
  font-size: var(--type-label);
  line-height: var(--type-label-lh);
  font-weight: var(--weight-semibold);
  text-transform: uppercase;
  letter-spacing: var(--track-label);
}

/* Losing the uppercase transform above also loses the visual weight/height
   caps give Latin at this size — at the shared 11px token, lowercase-shaped
   Devanagari reads noticeably smaller next to it (every kicker, masthead
   eyebrow, footer column heading, and mega-menu group title uses this
   class). A 1px bump (2026-08-30) proved imperceptible; +3px is the value
   that actually reads as parity with tracked Latin caps at this size, so
   Hindi gets that compensating bump rather than a uniform token change that
   would also shrink/grow Latin. */
.imloka-label:lang(hi) {
  font-size: calc(var(--type-label) + 3px);
  text-transform: none;
  letter-spacing: normal;
}

/* §Reading Measure: prose sits at ~640–720px in both scripts. Do not use
 * English character-count targets as a Hindi metric. */
.imloka-prose {
  max-width: var(--reading-max);
  font-size: var(--type-body);
  line-height: var(--type-body-lh);
}

.imloka-prose a {
  color: var(--link);
  text-decoration: underline;
  text-underline-offset: 0.18em;
}

/* Hindi emphasis uses weight and context, never synthetic oblique glyphs. */
:lang(hi) em,
:lang(hi) i {
  font-style: normal;
  font-weight: var(--weight-semibold);
}
}

/* ============================ @layer primitives */

/* --- styles/primitives/button.css */
@layer primitives {
/* Primitives: buttons. §Buttons, Forms, and Utility Components.
 *
 * "imloka controls should feel like part of the publication: not an
 * application framework placed on top of it."
 *
 * Ordinary radius 2–4px. Pills are reserved for semantic chips and toggles.
 * No default scale or bounce on press. */

.imloka-button {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-2);
  min-height: var(--target-min);
  padding-inline: var(--space-5, 20px);
  border: 1px solid transparent;
  border-radius: var(--radius-sm);
  font-size: var(--type-ui);
  font-weight: var(--weight-medium);
  line-height: 1;
  text-decoration: none;
  cursor: pointer;
  transition:
    background-color var(--motion-fast) var(--ease-standard),
    border-color var(--motion-fast) var(--ease-standard),
    color var(--motion-fast) var(--ease-standard);
}

.imloka-button--primary {
  background: var(--primary);
  color: var(--text-on-primary);
}
.imloka-button--primary:hover { background: var(--primary-strong); color: var(--text-on-primary); }

.imloka-button--secondary {
  background: var(--surface);
  border-color: var(--border);
  color: var(--text);
}
.imloka-button--secondary:hover { border-color: var(--primary); color: var(--primary); }

.imloka-button--ghost {
  background: none;
  color: var(--text);
}
.imloka-button--ghost:hover { background: var(--surface-alt); }

/* Disabled stays readable: never a 0.2 opacity wash. */
.imloka-button[disabled],
.imloka-button[aria-disabled="true"] {
  cursor: not-allowed;
  background: var(--surface-alt);
  border-color: var(--border);
  color: var(--text-muted);
}

/* Icon buttons carry a ~44x44 hit area even when the glyph is 20px. */
.imloka-icon-button {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: var(--target-min);
  height: var(--target-min);
  padding: 0;
  background: none;
  border: 0;
  border-radius: var(--radius-xs);
  color: var(--text);
  cursor: pointer;
  transition: background-color var(--motion-fast) var(--ease-standard);
}

.imloka-icon-button:hover { background: var(--surface-alt); }

.imloka-icon-button__icon { display: block; }

@media (forced-colors: active) {
  .imloka-button { border-color: currentColor; }
}
}

/* --- styles/primitives/controls.css */
@layer primitives {
/* Primitives: chips, segmented controls, theme swatches, inline notices.
 *
 * Pill geometry appears here and only here: topic chips, filter chips, and
 * toggles are the semantic cases the spec allows. Everything else stays at
 * 2–4px. */

.imloka-chip-row {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
}

.imloka-chip {
  display: inline-flex;
  align-items: center;
  min-height: 36px;
  padding-inline: var(--space-4);
  border: 1px solid var(--border);
  border-radius: var(--radius-pill);
  background: var(--surface);
  color: var(--text);
  font-size: var(--type-meta);
  font-weight: var(--weight-medium);
  text-decoration: none;
  transition:
    border-color var(--motion-fast) var(--ease-standard),
    background-color var(--motion-fast) var(--ease-standard),
    color var(--motion-fast) var(--ease-standard);
}

.imloka-chip:hover { border-color: var(--primary); color: var(--primary); }

/* A filter chip is a selection control, so it is a button and reports its
   state with aria-pressed. A topic chip is navigation, so it is a link. */
.imloka-chip[aria-pressed="true"] {
  background: var(--primary);
  border-color: var(--primary);
  color: var(--text-on-primary);
}

/* --- segmented control ---------------------------------------------------- */
/* Used for Appearance (System | Light | Dark) and the language pair. */

.imloka-segmented {
  display: inline-flex;
  padding: 3px;
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  background: var(--surface-alt);
}

.imloka-segmented__option {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-height: 38px;
  padding-inline: var(--space-4);
  border: 0;
  border-radius: var(--radius-xs);
  background: none;
  color: var(--text-muted);
  font-size: var(--type-meta);
  font-weight: var(--weight-medium);
  text-decoration: none;
  cursor: pointer;
  transition:
    background-color var(--motion-fast) var(--ease-standard),
    color var(--motion-fast) var(--ease-standard);
}

.imloka-segmented__option:hover { color: var(--text); }

/* Selected state carries weight and a surface change, not colour alone (C11). */
.imloka-segmented__option[aria-pressed="true"],
.imloka-segmented__option[aria-current="true"] {
  background: var(--surface);
  color: var(--text);
  font-weight: var(--weight-semibold);
  box-shadow: inset 0 0 0 1px var(--border);
}

/* --- theme picker --------------------------------------------------------- */

.imloka-theme-picker {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
}

.imloka-theme-swatch {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  min-height: var(--target-min);
  padding-inline: var(--space-3);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  background: var(--surface);
  color: var(--text-muted);
  font-size: var(--type-meta);
  font-weight: var(--weight-medium);
  cursor: pointer;
  transition:
    border-color var(--motion-fast) var(--ease-standard),
    color var(--motion-fast) var(--ease-standard);
}

.imloka-theme-swatch:hover { color: var(--text); border-color: var(--primary); }

.imloka-theme-swatch[aria-pressed="true"] {
  border-color: var(--primary);
  color: var(--text);
  box-shadow: inset 0 0 0 1px var(--primary);
}

/* Compact variant: dot only, name kept for assistive tech via
   .imloka-visually-hidden rather than shown. Used where the picker needs to
   read as a quiet row of swatches (Menu) rather than a labelled control list
   (Settings). */
.imloka-theme-swatch--dots {
  justify-content: center;
  width: var(--target-min);
  height: var(--target-min);
  min-height: 0;
  padding: 0;
  gap: 0;
  border-color: transparent;
  border-radius: var(--radius-pill);
  background: transparent;
}

.imloka-theme-swatch--dots:hover { border-color: var(--border); }

.imloka-theme-swatch--dots[aria-pressed="true"] {
  border-color: var(--primary);
  box-shadow: none;
}

/* The dot paints itself from the swatch's own data-theme attributes, so the
   preview is the real palette rather than a hard-coded approximation. */
.imloka-theme-swatch__dot {
  position: relative;
  width: 16px;
  height: 16px;
  border-radius: var(--radius-pill);
  background: var(--primary);
  box-shadow: inset 0 0 0 1px var(--border);
}

/* Selected reads as selected without colour: a check precedes the name. */
.imloka-theme-swatch[aria-pressed="true"] .imloka-theme-swatch__name::before {
  content: "✓ ";
}

/* The --dots variant hides the name, so the check moves onto the dot itself
   as a small neutral-surface badge rather than relying on the dot's own hue
   to carry selection (C11). */
.imloka-theme-swatch--dots .imloka-theme-swatch__dot { width: 22px; height: 22px; }

.imloka-theme-swatch--dots .imloka-theme-swatch__dot::after {
  content: "✓";
  position: absolute;
  right: -3px;
  bottom: -3px;
  width: 12px;
  height: 12px;
  line-height: 11px;
  text-align: center;
  font-size: 8px;
  font-weight: var(--weight-semibold);
  border-radius: 50%;
  background: var(--surface);
  color: var(--text);
  box-shadow: inset 0 0 0 1px var(--border);
  opacity: 0;
  transform: scale(.6);
  transition:
    opacity var(--motion-fast) var(--ease-standard),
    transform var(--motion-fast) var(--ease-standard);
}

.imloka-theme-swatch--dots[aria-pressed="true"] .imloka-theme-swatch__dot::after {
  opacity: 1;
  transform: scale(1);
}

/* --- inline notice -------------------------------------------------------- */

.imloka-inline-notice {
  padding: var(--space-4);
  border-radius: var(--radius-sm);
  border-inline-start: 3px solid var(--border);
  background: var(--surface-alt);
  font-size: var(--type-meta);
  line-height: 1.6;
  margin-bottom: var(--space-6);
}

.imloka-inline-notice[data-tone="warning"] {
  background: var(--warning-surface);
  border-inline-start-color: var(--warning);
  color: var(--warning);
}

.imloka-inline-notice[data-tone="danger"] {
  background: var(--danger-surface);
  border-inline-start-color: var(--danger);
  color: var(--danger);
}

.imloka-inline-notice[data-tone="success"] {
  background: var(--success-surface);
  border-inline-start-color: var(--success);
  color: var(--success);
}

@media (forced-colors: active) {
  .imloka-chip,
  .imloka-segmented__option[aria-pressed="true"],
  .imloka-theme-swatch[aria-pressed="true"] { border: 1px solid currentColor; }
}
}

/* --- styles/primitives/feedback.css */
@layer primitives {
/* Primitives: badge, divider, disclosure, popover, tooltip, toast, spinner,
 * tabs, breadcrumbs, pagination, load-more.
 * §Buttons, Forms, and Utility Components; §Motion and Interaction. */

/* --- text link ------------------------------------------------------------ */
/* Navigation is a link; a state-changing action is a button. The distinction
 * is semantic, not visual, and it is not negotiable. */

.imloka-text-link {
  color: var(--link);
  text-decoration: underline;
  text-underline-offset: 0.18em;
  text-decoration-thickness: 1px;
}

.imloka-text-link:hover { color: var(--link-hover); text-decoration-thickness: 2px; }

/* --- badge ---------------------------------------------------------------- */
/* Metadata, not a control: a badge never triggers a state change. Most are
 * plain <span>: pinned/author/staff markers, a comment's Pending status.
 * The article Format badge is the one exception, rendered as <a>: Format is a
 * taxonomy facet with its own real archive page, the same as the Subject/
 * Place kicker beside it, so it navigates like one: same appearance, an
 * added hover state, and the site's normal focus ring (§Focus, reset.css)
 * since it's a real link underneath. */

.imloka-badge {
  display: inline-flex;
  align-items: center;
  gap: var(--space-1);
  padding: 3px var(--space-2);
  border-radius: var(--radius-xs);
  background: var(--surface-alt);
  color: var(--text-muted);
  font-size: var(--type-label);
  font-weight: var(--weight-semibold);
  text-transform: uppercase;
  letter-spacing: var(--track-label);
}

a.imloka-badge { text-decoration: none; }
a.imloka-badge:hover { background: var(--border); color: var(--text); }

.imloka-badge:lang(hi) { text-transform: none; letter-spacing: normal; }

.imloka-badge--author { background: var(--secondary); color: var(--text); }
.imloka-badge--staff { background: var(--primary); color: var(--text-on-primary); }

/* Space and Zone sit beside the filled Topic/Format badges on an article's
   opening line: an outlined treatment (same bordered, --radius-xs box as
   the header's Search/Menu triggers) keeps all four legible as one group
   while making clear these two are a different kind of label (Mode and
   Length) rather than a third and fourth Format. Identical to each other,
   so neither reads as more load-bearing than the other. */
.imloka-badge--space,
.imloka-badge--zone {
  background: transparent;
  border-radius: var(--radius-xs);
  border: 1px solid var(--border);
  color: var(--text-muted);
}

a.imloka-badge--space:hover,
a.imloka-badge--zone:hover { background: var(--surface-alt); color: var(--text); }

/* --- divider -------------------------------------------------------------- */

.imloka-divider {
  border: 0;
  border-top: 1px solid var(--border);
  margin-block: var(--space-8);
}

/* --- disclosure ----------------------------------------------------------- */
/* A native <details> wherever possible; the marker is replaced, not the
 * element, so keyboard and screen-reader behaviour comes for free. */

.imloka-disclosure {
  border-bottom: 1px solid var(--border);
}

.imloka-disclosure__summary {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-4);
  min-height: var(--target-min);
  padding-block: var(--space-3);
  font-size: var(--type-ui);
  font-weight: var(--weight-medium);
  cursor: pointer;
  list-style: none;
}

.imloka-disclosure__summary::-webkit-details-marker { display: none; }

.imloka-disclosure__summary::after {
  content: "";
  flex: 0 0 auto;
  width: 9px;
  height: 9px;
  border-inline-end: 1.75px solid currentColor;
  border-block-end: 1.75px solid currentColor;
  transform: rotate(45deg) translate(-2px, -2px);
  transition: transform var(--motion-base) var(--ease-standard);
}

.imloka-disclosure[open] > .imloka-disclosure__summary::after {
  transform: rotate(-135deg) translate(-2px, -2px);
}

.imloka-disclosure__body {
  padding-bottom: var(--space-4);
  font-size: var(--type-body);
  color: var(--text-muted);
}

/* --- popover -------------------------------------------------------------- */
/* The one place 8px radius is correct: layered UI. */

.imloka-popover {
  position: absolute;
  z-index: var(--z-popover);
  /* Anchored to the trigger's inline start and clamped to the viewport. An
     absolutely-positioned element still contributes to scrollWidth even while
     transparent, so an unclamped popover silently overflows narrow screens
     without ever being visible. */
  inset-inline-start: 0;
  top: calc(100% + var(--space-2));
  min-width: min(220px, 100%);
  max-width: min(320px, calc(100vw - var(--space-16)));
  padding: var(--space-4);
  border: 1px solid var(--border);
  border-radius: var(--radius-layer);
  background: var(--surface);
  box-shadow: 0 8px 28px -12px var(--image-overlay);
  opacity: 0;
  transform: translateY(-4px);
  transition:
    opacity var(--motion-fast) var(--ease-exit),
    transform var(--motion-fast) var(--ease-exit);
}

.imloka-popover[data-state="open"] {
  opacity: 1;
  transform: translateY(0);
  /* Opening is slightly slower than closing. */
  transition:
    opacity var(--motion-base) var(--ease-enter),
    transform var(--motion-base) var(--ease-enter);
}

/* --- tooltip -------------------------------------------------------------- */
/* Nonessential clarification only. Nothing a reader must know may live here,
 * because hover is not available to every input method. */

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

.imloka-tooltip {
  position: absolute;
  bottom: calc(100% + 8px);
  inset-inline-start: 50%;
  translate: -50% 0;
  z-index: var(--z-popover);
  width: max-content;
  max-width: 240px;
  padding: var(--space-2) var(--space-3);
  border-radius: var(--radius-sm);
  background: var(--text);
  color: var(--page-bg);
  font-size: var(--type-meta);
  line-height: 1.4;
  pointer-events: none;
  opacity: 0;
  transition: opacity var(--motion-fast) var(--ease-standard);
}

/* ~400-600ms hover delay, so a tooltip never flashes as the pointer crosses. */
.imloka-tooltip-host:hover .imloka-tooltip { opacity: 1; transition-delay: 480ms; }
.imloka-tooltip-host:focus-within .imloka-tooltip { opacity: 1; transition-delay: 0ms; }

/* --- toast ---------------------------------------------------------------- */
/* Short background confirmation. Never a major decision: a toast that carries
 * a choice is a dialog wearing the wrong clothes. */

.imloka-toast-region {
  position: fixed;
  z-index: var(--z-toast);
  inset-block-end: var(--space-6);
  inset-inline-start: var(--space-6);
  display: grid;
  gap: var(--space-2);
  max-width: min(28rem, calc(100vw - var(--space-8)));
  pointer-events: none;
}

.imloka-toast {
  display: flex;
  align-items: flex-start;
  gap: var(--space-3);
  padding: var(--space-4);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  background: var(--surface);
  box-shadow: 0 8px 28px -14px var(--image-overlay);
  font-size: var(--type-meta);
  pointer-events: auto;
  opacity: 0;
  transform: translateY(8px);
  transition:
    opacity var(--motion-base) var(--ease-enter),
    transform var(--motion-base) var(--ease-enter);
}

.imloka-toast[data-state="open"] { opacity: 1; transform: translateY(0); }
.imloka-toast__icon { flex: 0 0 auto; color: var(--success); font-weight: var(--weight-semibold); }
.imloka-toast__body { flex: 1 1 auto; min-width: 0; }

@media (prefers-reduced-motion: reduce) {
  /* The toast still appears and still announces; only the travel is dropped. */
  .imloka-toast { transform: none; }
  .imloka-popover { transform: none; }
}

/* --- spinner and progress ------------------------------------------------- */

.imloka-spinner {
  display: inline-block;
  width: 18px;
  height: 18px;
  border: 2px solid var(--border);
  border-top-color: var(--primary);
  border-radius: var(--radius-pill);
  animation: imloka-spin var(--motion-spinner) var(--ease-linear) infinite;
}

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



.imloka-progress {
  height: 3px;
  background: var(--surface-alt);
  overflow: hidden;
}

.imloka-progress__bar {
  height: 100%;
  background: var(--primary);
  transition: width var(--motion-base) var(--ease-standard);
}

/* A loading button preserves its width, so the layout does not jump and the
 * reader does not lose the target they were aiming at. */
.imloka-button[data-loading="true"] {
  pointer-events: none;
  color: transparent;
  position: relative;
}

.imloka-button[data-loading="true"]::after {
  content: "";
  position: absolute;
  width: 16px;
  height: 16px;
  border: 2px solid currentColor;
  border-top-color: transparent;
  border-radius: var(--radius-pill);
  color: var(--text-on-primary);
  animation: imloka-spin var(--motion-spinner) var(--ease-linear) infinite;
}

.imloka-button--secondary[data-loading="true"]::after { color: var(--text); }

/* The loading button's ring is state, not decoration: it opts back in under
   reduced motion for the same reason the standalone spinner does. */
@media (prefers-reduced-motion: reduce) {
  .imloka-button[data-loading="true"]::after {
    animation-duration: var(--motion-spinner-reduced) !important;
    animation-iteration-count: infinite !important;
  }
}

/* --- tabs ----------------------------------------------------------------- */
/* Underline by default, not pills: pills are reserved for chips and toggles. */

.imloka-tabs {
  display: flex;
  gap: var(--space-6);
  border-bottom: 1px solid var(--border);
  overflow-x: auto;
}

.imloka-tab {
  position: relative;
  display: inline-flex;
  align-items: center;
  min-height: var(--target-min);
  padding-block: var(--space-2);
  border: 0;
  background: none;
  color: var(--text-muted);
  font-size: var(--type-ui);
  font-weight: var(--weight-medium);
  white-space: nowrap;
  text-decoration: none;
  cursor: pointer;
  transition: color var(--motion-fast) var(--ease-standard);
}

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

.imloka-tab[aria-selected="true"],
.imloka-tab[aria-current="page"] {
  color: var(--text);
  font-weight: var(--weight-semibold);
}

.imloka-tab[aria-selected="true"]::after,
.imloka-tab[aria-current="page"]::after {
  content: "";
  position: absolute;
  inset-inline: 0;
  bottom: -1px;
  height: 2px;
  background: var(--primary);
}

/* --- breadcrumbs ---------------------------------------------------------- */

.imloka-breadcrumbs { font-size: var(--type-meta); color: var(--text-muted); }
.imloka-breadcrumbs__list { display: flex; flex-wrap: wrap; gap: var(--space-2); }
.imloka-breadcrumbs__list li { display: flex; gap: var(--space-2); align-items: center; }
.imloka-breadcrumbs__list li + li::before { content: "/"; color: var(--border); }
.imloka-breadcrumbs a { color: var(--text-muted); text-decoration: none; }
.imloka-breadcrumbs a:hover { color: var(--primary); text-decoration: underline; }

/* --- pagination and load more --------------------------------------------- */
/* Load More is preferred for archive continuation; stable pagination remains
 * valid. Neither may make the footer unreachable: no infinite scroll. */

.imloka-load-more {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--space-3);
  padding-block: var(--space-10);
}

.imloka-load-more__count { font-size: var(--type-meta); color: var(--text-muted); }

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

.imloka-pagination__link {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: var(--target-min);
  min-height: var(--target-min);
  padding-inline: var(--space-2);
  border-radius: var(--radius-xs);
  color: var(--text);
  font-size: var(--type-ui);
  text-decoration: none;
  font-variant-numeric: tabular-nums;
}

.imloka-pagination__link:hover { background: var(--surface-alt); }

.imloka-pagination__link[aria-current="page"] {
  background: var(--primary);
  color: var(--text-on-primary);
  font-weight: var(--weight-semibold);
}

/* --- auth dialog ---------------------------------------------------------- */
/* A native <dialog>: focus trapping, Esc, and inertness come from the platform
 * rather than being reimplemented, which is the spec's stated preference. */

.imloka-auth-dialog {
  width: min(30rem, calc(100vw - var(--space-8)));
  padding: 0;
  border: 1px solid var(--border);
  border-radius: var(--radius-layer);
  background: var(--surface);
  color: var(--text);
}

.imloka-auth-dialog::backdrop { background: var(--image-overlay); }

.imloka-auth-dialog__inner {
  display: grid;
  gap: var(--space-4);
  padding: var(--space-8);
}

.imloka-auth-dialog__actions {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-3);
  margin-top: var(--space-2);
}
}

/* --- styles/primitives/forms.css */
@layer primitives {
/* Primitives: form controls. §Buttons, Forms, and Utility Components.
 *
 * Native semantics are preferred to ARIA reinvention throughout: a real
 * <select>, a real <input type="checkbox">, a real <fieldset>. The styling
 * layer never replaces the control, only its appearance. */

.imloka-field {
  display: grid;
  gap: var(--space-2);
}

/* A visible, persistent label. A placeholder is never the only label: it
 * vanishes the moment someone types, taking the question with it. */
.imloka-field__label {
  font-size: var(--type-ui);
  font-weight: var(--weight-medium);
  color: var(--text);
}

.imloka-field__optional {
  color: var(--text-muted);
  font-weight: var(--weight-regular);
}

.imloka-field__hint {
  font-size: var(--type-meta);
  color: var(--text-muted);
}

.imloka-input,
.imloka-textarea,
.imloka-select {
  width: 100%;
  min-height: 46px;
  padding: var(--space-3);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  background: var(--surface);
  color: var(--text);
  /* 16px floor stops iOS zooming the page when the field takes focus. */
  font-size: max(16px, var(--type-ui));
  line-height: 1.4;
  transition:
    border-color var(--motion-fast) var(--ease-standard),
    background-color var(--motion-fast) var(--ease-standard);
}

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

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

.imloka-input:focus,
.imloka-textarea:focus,
.imloka-select:focus { border-color: var(--primary); }

/* Disabled stays readable. An opacity wash makes the value unreadable, which
 * is a different and worse thing than communicating "not editable". */
.imloka-input:disabled,
.imloka-textarea:disabled,
.imloka-select:disabled {
  background: var(--surface-alt);
  color: var(--text-muted);
  cursor: not-allowed;
}

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

/* The comment composer is deliberately taller: it invites a paragraph. */
.imloka-textarea--composer { min-height: 160px; }

.imloka-select {
  appearance: none;
  padding-inline-end: var(--space-10);
  /* The chevron is drawn with gradients rather than a background image so it
     inherits the theme and needs no asset. */
  background-image:
    linear-gradient(45deg, transparent 50%, currentColor 50%),
    linear-gradient(135deg, currentColor 50%, transparent 50%);
  background-position:
    calc(100% - 20px) calc(50% + 2px),
    calc(100% - 15px) calc(50% + 2px);
  background-size: 5px 5px, 5px 5px;
  background-repeat: no-repeat;
}

/* --- error state ---------------------------------------------------------- */
/* Errors carry specific text and a structural marker, never colour alone. */

.imloka-input[aria-invalid="true"],
.imloka-textarea[aria-invalid="true"],
.imloka-select[aria-invalid="true"] {
  border-color: var(--danger);
  border-inline-start-width: 3px;
}

.imloka-field__error {
  display: flex;
  align-items: flex-start;
  gap: var(--space-2);
  font-size: var(--type-meta);
  font-weight: var(--weight-medium);
  color: var(--danger);
}

.imloka-field__error::before {
  content: "✗";
  flex: 0 0 auto;
}

/* An error summary for longer forms: one place that names every problem and
 * links to it, so a keyboard or screen-reader user is not left hunting. */
.imloka-error-summary {
  padding: var(--space-4);
  border: 1px solid var(--danger);
  border-inline-start-width: 3px;
  border-radius: var(--radius-sm);
  background: var(--danger-surface);
  margin-bottom: var(--space-6);
}

.imloka-error-summary__title {
  color: var(--danger);
  margin-bottom: var(--space-2);
}

/* Each entry jumps to the field it names, so it is a navigation link and owes
   the minimum target: the reader reaching for it is already having a bad time. */
.imloka-error-summary a {
  display: inline-flex;
  align-items: center;
  min-height: var(--target-min);
  color: var(--danger);
}

/* --- checkbox and radio --------------------------------------------------- */

.imloka-choice {
  display: flex;
  align-items: flex-start;
  gap: var(--space-3);
  min-height: var(--target-min);
  padding-block: var(--space-2);
  cursor: pointer;
}

.imloka-choice__control {
  flex: 0 0 auto;
  width: 20px;
  height: 20px;
  margin-top: 2px;
  appearance: none;
  border: 1px solid var(--border);
  background: var(--surface);
  cursor: pointer;
  display: grid;
  place-content: center;
  transition:
    border-color var(--motion-fast) var(--ease-standard),
    background-color var(--motion-fast) var(--ease-standard);
}

input.imloka-choice__control[type="checkbox"] { border-radius: var(--radius-xs); }
input.imloka-choice__control[type="radio"] { border-radius: var(--radius-pill); }

.imloka-choice__control:checked {
  background: var(--primary);
  border-color: var(--primary);
}

.imloka-choice__control[type="checkbox"]:checked::after {
  content: "";
  width: 10px;
  height: 6px;
  border-inline-start: 2px solid var(--text-on-primary);
  border-block-end: 2px solid var(--text-on-primary);
  transform: rotate(-45deg) translate(1px, -1px);
}

.imloka-choice__control[type="radio"]:checked::after {
  content: "";
  width: 8px;
  height: 8px;
  border-radius: var(--radius-pill);
  background: var(--text-on-primary);
}

.imloka-choice__control:disabled { background: var(--surface-alt); cursor: not-allowed; }
.imloka-choice__control:disabled + .imloka-choice__text { color: var(--text-muted); }

.imloka-choice__text { font-size: var(--type-ui); line-height: 1.5; }
.imloka-choice__note { display: block; font-size: var(--type-meta); color: var(--text-muted); }

.imloka-fieldset {
  border: 0;
  padding: 0;
  margin: 0;
}

.imloka-fieldset__legend {
  padding: 0;
  margin-bottom: var(--space-2);
  font-size: var(--type-ui);
  font-weight: var(--weight-medium);
}

/* --- toggle --------------------------------------------------------------- */
/* Pill geometry is correct here: a toggle is one of the two semantic cases the
 * spec allows. A toggle applies immediately: it never needs a Save button. */

.imloka-toggle {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-4);
  min-height: var(--target-min);
  padding-block: var(--space-2);
  cursor: pointer;
}

.imloka-toggle__text { font-size: var(--type-ui); }
.imloka-toggle__note { display: block; font-size: var(--type-meta); color: var(--text-muted); }

.imloka-toggle__switch {
  position: relative;
  flex: 0 0 auto;
  width: 46px;
  height: 26px;
  appearance: none;
  border: 1px solid var(--border);
  border-radius: var(--radius-pill);
  background: var(--surface-alt);
  cursor: pointer;
  transition:
    background-color var(--motion-base) var(--ease-standard),
    border-color var(--motion-base) var(--ease-standard);
}

.imloka-toggle__switch::after {
  content: "";
  position: absolute;
  top: 2px;
  inset-inline-start: 2px;
  width: 20px;
  height: 20px;
  border-radius: var(--radius-pill);
  background: var(--text-muted);
  transition:
    transform var(--motion-base) var(--ease-standard),
    background-color var(--motion-base) var(--ease-standard);
}

.imloka-toggle__switch:checked {
  background: var(--primary);
  border-color: var(--primary);
}

.imloka-toggle__switch:checked::after {
  background: var(--text-on-primary);
  transform: translateX(20px);
}

.imloka-toggle__switch:disabled { cursor: not-allowed; opacity: .7; }

@media (forced-colors: active) {
  .imloka-choice__control,
  .imloka-toggle__switch { border: 1px solid CanvasText; }
  .imloka-choice__control:checked,
  .imloka-toggle__switch:checked { background: Highlight; }
}
}

/* ============================ @layer components */

/* --- styles/components/content.css */
@layer components {
/* Components: the content layer. §Component Architecture.
 *
 * imloka deliberately does NOT have one universal Card that renders a story, a
 * comment, an advertisement, an author and an alert through thirty props. Each
 * of those means something different, and a component that cannot tell them
 * apart guarantees the design system cannot either. */

/* --- StoryListItem -------------------------------------------------------- */
/* The compact treatment used by archives, search results, and Latest. Never
 * homepage mosaic geometry. */

/* Single column until there's room for two: plain grid auto-placement, so
   items flow left-to-right then top-to-bottom (row-major) rather than the
   homepage mosaic's "Latest" tile, which deliberately fills one column
   before the next (see modules/list.css) to keep two independently-sized
   lists from stretching to match each other's height. A full-width listing
   page has no such height-matching problem (every item here is the same
   shape), so plain grid columns are the simpler, correct choice. */
.imloka-story-list { display: grid; }

@media (min-width: 700px) {
  .imloka-story-list { grid-template-columns: repeat(2, minmax(0, 1fr)); column-gap: var(--space-8); }
}

.imloka-story-item {
  position: relative;
  display: grid;
  gap: var(--space-1);
  padding-block: var(--space-5, 20px);
  border-bottom: 1px solid var(--border);
}

.imloka-story-item__kicker { color: var(--text-muted); }

.imloka-story-item__title {
  font-size: var(--type-heading-s);
  line-height: var(--type-heading-s-lh);
  font-weight: var(--weight-semibold);
  letter-spacing: var(--track-heading);
}

.imloka-story-item__title a {
  color: var(--text);
  text-decoration: none;
}

/* Whole-row click via a stretched link, the same contract the mosaic tile uses:
   a real anchor, a real accessible name, and a focus ring around the row rather
   than a zero-size pseudo-element. Without this the target is the title's own
   text box: 20px on a single-line headline, well under the minimum. */
.imloka-story-item__title a::after {
  content: "";
  position: absolute;
  inset: 0;
}

.imloka-story-item:has(a:focus-visible) {
  outline: 2px solid var(--focus);
  outline-offset: 2px;
}

.imloka-story-item__title a:focus-visible { outline: none; }

/* Anything else interactive in the row (Remove on Saved, for one) has to sit
   above the stretched link or it becomes unclickable. */
.imloka-story-item button,
.imloka-story-item a:not(.imloka-story-item__title a) {
  position: relative;
  z-index: var(--z-raised);
}

.imloka-story-item__title a:hover {
  color: var(--primary);
  text-decoration: underline;
  text-underline-offset: 0.16em;
}

.imloka-story-item__deck {
  font-size: var(--type-meta);
  color: var(--text-muted);
  line-height: 1.55;
}

.imloka-story-item__meta {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-1) var(--space-2);
  font-size: var(--type-meta);
  color: var(--text-muted);
}

/* A list item with a thumbnail: the image is secondary, so it never grows to
   dominate the row. */
.imloka-story-item--media {
  grid-template-columns: 84px minmax(0, 1fr);
  gap: var(--space-4);
  align-items: start;
}

.imloka-story-item--media .imloka-story-item__body { display: grid; gap: var(--space-1); }

.imloka-story-item__thumb {
  width: 84px;
  height: 60px;
  object-fit: cover;
  border-radius: var(--radius-xs);
  background: var(--surface-alt);
}

/* A story with no image anywhere — no curated thumbnail, no article hero —
   shows the app mark in the reserved thumbnail slot instead of an empty box. */
.imloka-story-item__thumb--icon {
  display: flex;
  align-items: center;
  justify-content: center;
}

.imloka-story-item__thumb--icon .imloka-mark {
  width: clamp(16px, 28%, 32px);
  height: auto;
  color: var(--text-muted);
}

@media (min-width: 768px) {
  .imloka-story-item--media { grid-template-columns: 132px minmax(0, 1fr); }
  .imloka-story-item__thumb { width: 132px; height: 88px; }
}

/* --- AuthorIdentity ------------------------------------------------------- */
/* Editorial identity, not a social profile. Portraits are rectangular by
 * default: the spec is explicit that circular avatars are not required, and a
 * forced circle crops editorial photography badly. */

.imloka-author {
  display: flex;
  align-items: center;
  gap: var(--space-3);
}

.imloka-author__portrait {
  flex: 0 0 auto;
  width: 44px;
  height: 44px;
  object-fit: cover;
  border-radius: var(--radius-xs);
  background: var(--surface-alt);
}

.imloka-author__name {
  /* A byline author link is a standalone navigation destination, not a word in
     a sentence, so it owes the minimum target. inline-flex keeps it on the
     byline row; the min-height grows the hit area, not the type. */
  display: inline-flex;
  align-items: center;
  min-height: var(--target-min);
  font-size: var(--type-ui);
  font-weight: var(--weight-medium);
  color: var(--text);
  text-decoration: none;
}

.imloka-author__name:hover { color: var(--primary); text-decoration: underline; text-underline-offset: 0.16em; }
.imloka-author__role { font-size: var(--type-meta); color: var(--text-muted); }

/* No portrait produces a deliberate text-led identity, not a broken placeholder
   or a grey silhouette. */
.imloka-author--text-led .imloka-author__portrait { display: none; }

/* --- Comment -------------------------------------------------------------- */
/* Text-first. No GIF uploads, no reaction bars, no social-network styling.
 * Threading is shallow: one or two visible indentation levels, flattening at
 * narrow widths rather than marching off the screen. */

.imloka-comment {
  padding-block: var(--space-5, 20px);
  border-bottom: 1px solid var(--border);
}

.imloka-comment__head {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: var(--space-2) var(--space-3);
  margin-bottom: var(--space-2);
}

.imloka-comment__author { font-size: var(--type-ui); font-weight: var(--weight-semibold); }
.imloka-comment__time { font-size: var(--type-meta); color: var(--text-muted); }
.imloka-comment__body { font-size: var(--type-body); line-height: 1.6; }
.imloka-comment__body p + p { margin-top: 0.8em; }

.imloka-comment__actions {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-1);
  margin-top: var(--space-2);
}

.imloka-comment__action {
  display: inline-flex;
  align-items: center;
  min-height: 36px;
  padding-inline: var(--space-2);
  border: 0;
  border-radius: var(--radius-xs);
  background: none;
  color: var(--text-muted);
  font-size: var(--type-meta);
  font-weight: var(--weight-medium);
  cursor: pointer;
  transition: color var(--motion-fast) var(--ease-standard);
}

.imloka-comment__action:hover { color: var(--primary); }

.imloka-comment__replies {
  margin-top: var(--space-4);
  padding-inline-start: var(--space-6);
  border-inline-start: 2px solid var(--border);
}

@media (max-width: 560px) {
  /* Deeper replies flatten at narrow widths and high zoom rather than
     compressing the reading column to nothing. */
  .imloka-comment__replies {
    padding-inline-start: var(--space-3);
  }
  .imloka-comment__replies .imloka-comment__replies {
    padding-inline-start: 0;
    border-inline-start: 0;
  }
}

/* A removed parent keeps a structural placeholder, so its replies do not
   become orphans with no context. */
.imloka-comment--removed .imloka-comment__body {
  color: var(--text-muted);
  font-style: italic;
}

.imloka-comment--pending {
  border-inline-start: 3px solid var(--warning);
  padding-inline-start: var(--space-4);
  background: var(--warning-surface);
}

/* --- NewsletterForm ------------------------------------------------------- */
/* A utility module, not an advertisement. It is the preferred strong 1x2
 * anchor in the mosaic, which is why it is styled to hold vertical space. */

.imloka-newsletter {
  /* Its own container, so the form below recomposes correctly wherever the
     block is used: inside a mosaic tile, in the article end sequence, or full
     width on the newsletter page. */
  container-type: inline-size;
  display: grid;
  gap: var(--space-4);
  align-content: center;
  padding: var(--space-8);
  border-radius: var(--radius-sm);
  background: var(--secondary);
  color: var(--text);
}

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

.imloka-newsletter__form { display: grid; gap: var(--space-3); }

/* Side by side only when the FORM has room, not when the window does.
 *
 * This used to be a viewport media query, which is the wrong question: inside a
 * mosaic tile the form is about 265px wide on a 1440px screen, so it went
 * two-column in a space that could not hold two columns. The `auto` track then
 * took the field's full width, the `1fr` track collapsed to 0, and the Subscribe
 * button (placed into row 1 with no column, so the auto-placer put it first)
 * rendered on top of the input.
 *
 * Both tracks are now placed explicitly. Auto-placement in a two-track grid is
 * exactly where an implicit rule quietly disagrees with the layout you meant. */
@container (min-width: 420px) {
  .imloka-newsletter__form { grid-template-columns: minmax(0, 1fr) auto; align-items: start; }
  .imloka-newsletter__form > .imloka-field { grid-column: 1; grid-row: 1; }
  .imloka-newsletter__form > .imloka-button { grid-column: 2; grid-row: 1; }
  .imloka-newsletter__form > .imloka-newsletter__note { grid-column: 1 / -1; grid-row: 2; }
}

.imloka-newsletter__note { font-size: var(--type-meta); color: var(--text-muted); }

/* The in-article slot. Kept to the reading measure and given generous space
   above and below, so it reads as an interruption the reader can skip rather
   than as the next paragraph. */
.imloka-ad--inline {
  width: min(100% - var(--gutter) * 2, var(--reading-max));
  margin-block: var(--space-12);
  margin-inline: auto;
}

/* --- AdSlot --------------------------------------------------------------- */
/* Advertising may participate in the layout, but it must never impersonate
 * editorial content. Geometry is reserved up front so a late-arriving ad (or
 * one that never arrives) cannot shift the text a reader is already reading. */

.imloka-ad {
  display: grid;
  gap: var(--space-2);
  padding: var(--space-4);
  background: var(--commercial-surface);
  border: 1px solid var(--border);
  border-radius: var(--radius-xs);
  /* Reserved height prevents CLS. */
  min-height: 148px;
  align-content: center;
  justify-items: center;
  text-align: center;
}

/* Disclosure is textual and structural. It must never depend on optional
   styling or an async response, because the label is release-critical data. */
.imloka-ad__label {
  color: var(--text-muted);
  align-self: start;
  justify-self: start;
}

.imloka-ad__body { color: var(--text-muted); font-size: var(--type-meta); }

/* An empty or failed slot collapses quietly or becomes a house promo. The
   reader never sees "Advertisement failed to load". */
.imloka-ad[data-state="empty"],
.imloka-ad[data-state="failed"] { display: none; }

.imloka-ad[data-state="house-promo"] {
  background: var(--surface-alt);
  border-style: dashed;
}

/* --- SponsoredStory ------------------------------------------------------- */
/* A distinct content class with immutable disclosure, not a story with a tag. */

.imloka-sponsored {
  display: grid;
  gap: var(--space-2);
  padding: var(--space-5, 20px);
  background: var(--commercial-surface);
  border: 1px solid var(--border);
  border-radius: var(--radius-xs);
}

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

.imloka-sponsored__title {
  font-size: var(--type-heading-s);
  line-height: var(--type-heading-s-lh);
  font-weight: var(--weight-semibold);
}

.imloka-sponsored__title a { color: var(--text); text-decoration: none; }
.imloka-sponsored__title a:hover { text-decoration: underline; text-underline-offset: 0.16em; }

.imloka-sponsored__disclosure {
  font-size: var(--type-meta);
  color: var(--text-muted);
  line-height: 1.5;
}
}

/* ============================ @layer modules */

/* --- styles/modules/list.css */
@layer modules {
/* Modules: the compact list treatments used inside tiles.
 * §Content Tile and Module Taxonomy. */

.imloka-module-list {
  display: grid;
  gap: var(--space-1);
  margin-top: var(--space-2);
}

.imloka-module-list__item {
  display: grid;
  gap: 2px;
  padding-block: var(--space-2);
  border-bottom: 1px solid var(--border);
}

.imloka-module-list__item:last-child { border-bottom: 0; }

.imloka-module-list__link {
  display: flex;
  align-items: center;
  min-height: 36px;
  font-size: var(--type-ui);
  font-weight: var(--weight-medium);
  color: var(--text);
  text-decoration: none;
}

/* Capped to one line, with a real ellipsis rather than an uncontrolled wrap.
   A wrapped title is exactly what let a long/short pair land unevenly in
   the same row above (see the multicol note below): a length limit closes
   that off at the source, and keeps every row the same height regardless
   of the two-column layout mode. */
.imloka-module-list__link-text {
  display: -webkit-box;
  -webkit-line-clamp: 1;
  -webkit-box-orient: vertical;
  overflow: hidden;
}

.imloka-module-list__link:hover {
  color: var(--primary);
  text-decoration: underline;
  text-underline-offset: 0.16em;
}

.imloka-module-list__meta {
  font-size: var(--type-meta);
  color: var(--text-muted);
}

/* Latest runs its list in two columns wherever it lands (2x1 or 2x2: both
   are 6 columns wide at desktop, so both have the room), rather than
   becoming a tall thin stack. A 2x2 Latest with an unsplit list learned this
   the hard way: at full stack height it needed more than the two rows it
   spans, pushing the whole shared row taller and taking its 1x1 row-mates
   past the tall-tile floor with it. The split itself (which items land in
   which half) is decided once at build time (see latestCol1/latestCol2 in
   build.mjs) rather than left to the browser via CSS grid or `columns`.
   Both were tried and both let two items of different rendered height land
   unevenly: a grid pairs items row-by-row and stretches both cells to the
   taller one (breaks under a mid-load webfont swap); `columns: 2` balances
   by measured column height, which can strand an item in the second column
   with visible empty space under the first. Two independent lists laid out
   side by side have nothing left to measure or balance: the count in each
   is fixed. */
@media (min-width: 768px) {
  .imloka-tile[data-module="Latest Posts"] .imloka-module-list-columns {
    display: flex;
    /* Flex's default align-items:stretch would grow the shorter list's <ul>
       to match the taller one's height; and since each <ul> is itself a
       grid, that leftover height lands as extra space between its own
       rows, splitting a 2-item column open around the middle. start
       keeps each column at its own natural height. */
    align-items: flex-start;
    gap: 0 var(--space-6);
  }

  .imloka-tile[data-module="Latest Posts"] .imloka-module-list-columns .imloka-module-list {
    flex: 1 1 0;
    min-width: 0;
  }

  /* Column 1 alone (nothing overflowed into column 2: see latestCol1/
     latestCol2 in build.mjs) reads as a single, deliberately one-column
     list rather than a two-column layout with an empty half beside it. */
  .imloka-tile[data-module="Latest Posts"] .imloka-module-list-columns .imloka-module-list:only-child {
    flex-basis: 100%;
    max-width: 32rem;
  }
}

/* The Latest module's own way out: a quiet link to the full Latest listing,
   pinned to the tile's bottom-right corner. margin-top:auto claims whatever
   space the list didn't use, so the button sits at the bottom regardless of
   how many items render above it: the tile's own flex-column body already
   does the rest. */
.imloka-tile__cta {
  align-self: flex-end;
  margin-top: auto;
}

/* A commercial tile carries no editorial padding: the disclosed slot fills it. */
.imloka-tile--commercial { background: none; }
.imloka-tile--commercial .imloka-ad { height: 100%; border-radius: var(--radius-xs); }
}

/* ============================ @layer patterns */

/* --- styles/patterns/archive.css */
@layer patterns {
/* Patterns: the News/Journal/Format/Place/Topic destination pages
 * (§Category and Topic Pages in the base spec; reclassified per
 * imloka_Content_Schema_Supplement_v1.md).
 *
 * Every one of them is a focused discovery destination: a masthead, then
 * an efficient archive of matching stories, never an endless wall of tiles. */

.imloka-masthead {
  display: grid;
  gap: var(--space-3);
  padding-block: var(--space-12) var(--space-10);
  max-width: 52rem;
}

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

/* An untranslated English eyebrow on a Hindi page (Space, Zone) takes the Latin
   label treatment. type.css's `.imloka-label:lang(hi)` matches this <p> itself
   (it inherits lang="hi" from <html>) and drops the caps and tracking; a
   lang="en" child only inherits that result, because a parent's :lang() match
   says nothing about the child's language. So the child needs its own rule.
   Deliberately scoped to the eyebrow rather than to .imloka-label generally:
   the menu carries `आपका <span lang="en">imloka</span>` in a label on 128 Hindi
   pages, and the wordmark is lowercase by design. */
.imloka-masthead__eyebrow :lang(en) {
  text-transform: uppercase;
  letter-spacing: var(--track-label);
}
.imloka-masthead__description { color: var(--text-muted); }
.imloka-masthead__topics { margin-top: var(--space-2); }

@media (min-width: 1024px) {
  .imloka-masthead { padding-block: var(--space-16) var(--space-12); }
}

.imloka-archive { padding-top: var(--space-16); }
.imloka-archive__title { margin-bottom: var(--space-6); }

/* A Topic page is quieter and more functional than a mode/format opening. */
.imloka-topic-related {
  padding-block: var(--space-12);
  border-top: 1px solid var(--border);
}

.imloka-topic-related__title { color: var(--text-muted); margin-bottom: var(--space-4); }
}

/* --- styles/patterns/article.css */
@layer patterns {
/* Patterns: the article reading experience. §Article and Reading Experience.
 *
 *   "Discovery gets the reader to the Article. The Article gets out of the
 *    reader's way."
 *
 * Article pages are calmer than discovery surfaces, and they are independent
 * external-entry destinations: a reader arriving from search, social, or a
 * shared link must understand the page without ever having seen Home. */

.imloka-article { padding-block: var(--space-8) var(--space-16); }

/* --- opening -------------------------------------------------------------- */
/* Canonical order: kicker → headline → deck → author · date · read time →
 * hero (optional) → body. Not negotiable, because it is what makes a
 * direct-entry article legible. */

.imloka-article__opening {
  display: grid;
  gap: var(--space-4);
  /* min() rather than max-width alone. A bare max-width larger than the
     viewport leaves the element flush to both edges, which is what the opening,
     the actions row and the end mark all did below ~1000px: headline, deck and
     byline sat hard against the screen edge while the body text they introduce
     was correctly inset. These four are the only blocks in the article that are
     NOT inside .imloka-shell, so nothing else was padding them.
     The subtraction only bites once the viewport is narrower than the cap, so
     the desktop measure is unchanged. */
  width: min(100% - var(--gutter) * 2, 62.5rem);   /* ~1000px: headline measure ceiling */
  margin-inline: auto;
  padding-block: var(--space-6) var(--space-10);
}

.imloka-article__kicker {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-3);
}

.imloka-article__kicker a {
  color: var(--primary);
  text-decoration: none;
  font-weight: var(--weight-semibold);
}

.imloka-article__kicker a:hover { text-decoration: underline; text-underline-offset: 0.18em; }

.imloka-article__headline {
  font-size: clamp(2.25rem, 1.2rem + 4vw, 4rem);
  line-height: 1.07;
  font-weight: var(--weight-semibold);
  letter-spacing: var(--track-display);
}

/* Devanagari headlines need the vertical room and take no negative tracking. */
:lang(hi) .imloka-article__headline { line-height: 1.24; letter-spacing: normal; }

.imloka-article__deck {
  font-size: var(--type-body-l);
  line-height: var(--type-body-l-lh);
  color: var(--text-muted);
  max-width: 46rem;
}

.imloka-article__byline {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-2) var(--space-3);
  padding-top: var(--space-2);
  font-size: var(--type-meta);
  color: var(--text-muted);
}

.imloka-article__byline a {
  color: var(--text);
  font-weight: var(--weight-medium);
  text-decoration: none;
}

.imloka-article__byline a:hover { color: var(--primary); text-decoration: underline; text-underline-offset: 0.16em; }

/* Space and Zone moved off the topic line onto the byline (article/opening.html),
   where the byline's own link rule would otherwise repaint them as ordinary
   metadata links: `patterns` loads after `primitives`, so the layer wins over
   feedback.css no matter what the specificity says. They are badges first and
   links second, so the treatment is restated here, in the layer that broke it,
   rather than by inflating specificity in the primitive. */
.imloka-article__byline a.imloka-badge {
  color: var(--text-muted);
  font-weight: var(--weight-semibold);
}

.imloka-article__byline a.imloka-badge:hover {
  color: var(--text);
  text-decoration: none;
}

/* The translation link is a restrained text link beside the article's identity:
   never a large CTA, a pill, or a persistent global banner. It renders only
   when a published editorial translation actually exists. */
.imloka-article__translation {
  display: inline-flex;
  align-items: center;
  min-height: var(--target-min);
  gap: var(--space-2);
  color: var(--link);
  text-decoration: underline;
  text-underline-offset: 0.18em;
}

/* --- news mode: update structures ----------------------------------------- */
/* News prioritises clarity, speed, and timestamp visibility. Update status is
 * semantic and explicit, never signalled by colour alone. */

.imloka-article__updated {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  font-weight: var(--weight-semibold);
  color: var(--text);
}

.imloka-update {
  display: grid;
  gap: var(--space-2);
  padding: var(--space-5, 20px);
  margin-block: var(--space-8);
  border-inline-start: 3px solid var(--primary);
  background: var(--surface-alt);
  border-radius: var(--radius-xs);
}

.imloka-update__time {
  font-size: var(--type-label);
  font-weight: var(--weight-semibold);
  text-transform: uppercase;
  letter-spacing: var(--track-label);
  color: var(--text-muted);
}

.imloka-update__time:lang(hi) { text-transform: none; letter-spacing: normal; }

/* --- hero ----------------------------------------------------------------- */
/* Hero is optional. A failed hero produces a text-first article, which is a
 * first-class outcome rather than a degraded one. */

.imloka-article__hero { margin-bottom: var(--space-10); }
.imloka-article__hero img { width: 100%; height: auto; border-radius: var(--radius-xs); }

.imloka-article__hero--wide { max-width: 75rem; margin-inline: auto; }

/* A specific template MAY set an aspect ratio where it helps: what the spec
   forbids is binding one to a tile ROLE. A wide hero crops to an editorial
   letterbox rather than running to the full height of whatever file was
   uploaded; the focal point decides what survives the crop. */
.imloka-article__hero--wide img {
  aspect-ratio: 16 / 9;
  object-fit: cover;
  object-position: calc(var(--focal-x, .5) * 100%) calc(var(--focal-y, .5) * 100%);
}

@media (max-width: 767px) {
  /* Narrow screens get a squarer crop so the subject stays legible. */
  .imloka-article__hero--wide img { aspect-ratio: 4 / 3; }
}
.imloka-article__hero--column { max-width: var(--reading-max); margin-inline: auto; }

/* The image may run to the edge of the screen; its caption may not.
   A full-bleed hero is a deliberate editorial choice, but the caption beneath
   it is prose, and prose flush against the viewport edge reads as a mistake
   whatever the picture above it is doing. padding-inline rather than a width
   cap, so the caption stays aligned with an inset figure's own edges. */
.imloka-figcaption {
  display: grid;
  gap: var(--space-1);
  margin-top: var(--space-3);
  padding-inline: var(--gutter);
  font-size: var(--type-meta);
  color: var(--text-muted);
  line-height: 1.5;
}

/* An already-inset figure has the gutter from its own width, so its caption
   would otherwise be indented twice over. */
.imloka-block--wide > .imloka-figcaption,
.imloka-article__hero:not(.imloka-article__hero--wide) > .imloka-figcaption,
.imloka-article__body .imloka-figcaption { padding-inline: 0; }

/* Caption and credit are separate fields, not one string: the CMS models them
   separately and so does the markup. */
.imloka-figcaption__credit { opacity: .8; }

/* --- prose ---------------------------------------------------------------- */

.imloka-article__body {
  max-width: var(--reading-max);
  margin-inline: auto;
  font-size: var(--type-body);
  line-height: var(--type-body-lh);
}

.imloka-article__body > * + * { margin-top: 1.5em; }

.imloka-article__body h2 {
  font-size: var(--type-heading-l);
  line-height: var(--type-heading-l-lh);
  font-weight: var(--weight-semibold);
  letter-spacing: var(--track-heading);
  margin-top: 2.2em;
  /* A Contents link jumps straight to the heading's own top edge; without
     this the sticky Header (same offset the Contents rail itself uses) sits
     over it. */
  scroll-margin-top: calc(var(--header-h-compact) + var(--space-6));
}

.imloka-article__body h3 {
  font-size: var(--type-heading-m);
  line-height: var(--type-heading-m-lh);
  font-weight: var(--weight-semibold);
  margin-top: 1.8em;
  scroll-margin-top: calc(var(--header-h-compact) + var(--space-6));
}

/* Article links are Primary plus underline: both, always. */
.imloka-article__body a {
  color: var(--link);
  text-decoration: underline;
  text-underline-offset: 0.18em;
}

.imloka-article__body a:hover { color: var(--link-hover); }

.imloka-article__body strong { font-weight: var(--weight-semibold); }

/* --- block vocabulary ----------------------------------------------------- */

.imloka-block--wide {
  /* Images and graphics may break the reading column, but stay on the grid. */
  width: min(75rem, calc(100vw - var(--gutter) * 2));
  margin-inline: calc(50% - min(37.5rem, calc(50vw - var(--gutter))));
}

.imloka-block img { width: 100%; height: auto; border-radius: var(--radius-xs); }

.imloka-gallery {
  display: grid;
  gap: var(--space-3);
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 18rem), 1fr));
}

.imloka-quote {
  margin-inline: 0;
  padding-inline-start: var(--space-6);
  border-inline-start: 3px solid var(--border);
  font-size: var(--type-body-l);
  line-height: 1.6;
}

.imloka-quote cite {
  display: block;
  margin-top: var(--space-3);
  font-size: var(--type-meta);
  font-style: normal;
  color: var(--text-muted);
}

/* A pullquote repeats something already in the text: it is emphasis, so it is
   aria-hidden to avoid reading the same sentence to a screen reader twice. */
.imloka-pullquote {
  margin-block: var(--space-12);
  padding-block: var(--space-6);
  border-block: 1px solid var(--border);
  font-size: clamp(1.35rem, 1rem + 1.2vw, 1.9rem);
  line-height: 1.32;
  font-weight: var(--weight-semibold);
  letter-spacing: var(--track-heading);
}

:lang(hi) .imloka-pullquote { line-height: 1.5; letter-spacing: normal; }

.imloka-callout {
  padding: var(--space-6);
  border-radius: var(--radius-sm);
  background: var(--surface-alt);
  border-inline-start: 3px solid var(--accent);
}

.imloka-callout__title { margin-bottom: var(--space-2); }

.imloka-article__body table {
  width: 100%;
  border-collapse: collapse;
  font-size: var(--type-meta);
}

.imloka-article__body th,
.imloka-article__body td {
  padding: var(--space-2) var(--space-3);
  text-align: left;
  border-bottom: 1px solid var(--border);
}

.imloka-article__body th { font-weight: var(--weight-semibold); color: var(--text-muted); }

/* Tables stay semantic and scroll locally rather than pushing the page sideways. */
.imloka-table-wrap { overflow-x: auto; }

.imloka-article__body pre {
  overflow-x: auto;
  padding: var(--space-4);
  border-radius: var(--radius-sm);
  background: var(--surface-alt);
  font-size: var(--type-meta);
  line-height: 1.6;
}

.imloka-article__body code {
  font-family: ui-monospace, "SF Mono", Menlo, Consolas, monospace;
  font-size: 0.92em;
}

.imloka-embed {
  position: relative;
  border-radius: var(--radius-sm);
  overflow: hidden;
  background: var(--surface-alt);
}

/* Media is reader-controlled: nothing autoplays, and a poster stands in until
   the reader asks for the media itself. */
.imloka-media-placeholder {
  display: grid;
  gap: var(--space-3);
  place-items: center;
  min-height: 220px;
  padding: var(--space-6);
  text-align: center;
  color: var(--text-muted);
}

.imloka-footnotes {
  margin-top: var(--space-12);
  padding-top: var(--space-6);
  border-top: 1px solid var(--border);
  font-size: var(--type-meta);
  line-height: 1.6;
  color: var(--text-muted);
}

.imloka-footnotes ol { padding-inline-start: 1.3em; }
.imloka-footnotes li + li { margin-top: var(--space-2); }

/* --- table of contents ---------------------------------------------------- */

.imloka-article__layout { position: relative; }

@media (min-width: 1200px) {
  .imloka-article__layout {
    display: grid;
    grid-template-columns: minmax(0, var(--rail-w)) minmax(0, 1fr) minmax(0, var(--rail-w));
    gap: var(--space-8);
    align-items: start;
  }
  .imloka-article__layout > .imloka-article__body { grid-column: 2; }
  .imloka-toc { grid-column: 1; grid-row: 1; }

  /* A wide block breaks out of the reading column; but only to the RIGHT.
   *
   * Its default breakout is computed against the viewport (50vw), which is
   * correct while the article is one centred column and wrong the moment a
   * sticky Contents rail appears beside it: the chart reached back across the
   * whole page and ran underneath the table of contents.
   *
   * The right-hand rail is deliberately empty (it exists to balance the
   * Contents column), so that is where the extra width comes from. The block
   * keeps the body's left edge and extends into the rail, which is both
   * collision-free and uses space that was already reserved. */
  .imloka-article__layout .imloka-block--wide {
    width: auto;
    margin-inline: 0 calc(-1 * (var(--rail-w) + var(--space-8)));
  }
}

.imloka-toc {
  position: sticky;
  top: calc(var(--header-h-compact) + var(--space-6));
  font-size: var(--type-meta);
}

.imloka-toc__title { color: var(--text-muted); margin-bottom: var(--space-3); }
.imloka-toc__list { display: grid; gap: var(--space-1); }

.imloka-toc__link {
  /* Flex with a min-height rather than block: entries that wrap to two lines
     still centre, and a single-line entry still owes the minimum target. The
     spec's working minimum is ~44px; 36 is the floor, 44 on coarse pointers. */
  display: flex;
  align-items: center;
  min-height: 36px;
  padding: var(--space-1) var(--space-3);
  border-inline-start: 2px solid var(--border);
  color: var(--text-muted);
  text-decoration: none;
  line-height: 1.4;
  transition:
    color var(--motion-fast) var(--ease-standard),
    border-color var(--motion-fast) var(--ease-standard);
}

.imloka-toc__link:hover { color: var(--text); }

@media (pointer: coarse) {
  .imloka-toc__link { min-height: var(--target-min); }
}

/* Current section is weight plus a border marker, never colour alone. */
.imloka-toc__link[aria-current="true"] {
  color: var(--text);
  font-weight: var(--weight-semibold);
  border-inline-start-color: var(--primary);
}

/* Below the sticky breakpoint the TOC collapses into a disclosure. */
@media (max-width: 1199px) {
  .imloka-toc {
    position: static;
    max-width: var(--reading-max);
    margin: 0 auto var(--space-8);
  }
}

/* --- reading progress ----------------------------------------------------- */
/* Measures the article region only: not the whole page including comments and
 * footer, which would tell the reader they are further along than they are. */

.imloka-reading-progress {
  position: absolute;
  inset-block-end: -1px;
  inset-inline-start: 0;
  height: 3px;
  width: 0;
  background: var(--primary);
  transition: width var(--motion-instant) var(--ease-linear);
}

/* --- actions and article end ---------------------------------------------- */
/* Compact actions. Save may carry more emphasis than Share. No floating social
 * rail and no persistent mobile action bar. */

.imloka-article__actions {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-3);
  width: min(100% - var(--gutter) * 2, var(--reading-max));
  margin: var(--space-10) auto 0;
  padding-top: var(--space-6);
  border-top: 1px solid var(--border);
}

.imloka-article__end {
  width: min(100% - var(--gutter) * 2, var(--reading-max));
  margin: var(--space-12) auto var(--space-8);
  padding-top: var(--space-6);
  border-top: 1px solid var(--border);
}

.imloka-article__end-mark {
  color: var(--text-muted);
}

/* A sibling of __end, not a child of it; so it needs the gutter in its own
   right. Easy to miss precisely because it renders directly beneath one. */
.imloka-article__topics {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-2);
  width: min(100% - var(--gutter) * 2, var(--reading-max));
  margin: 0 auto var(--space-12);
}

.imloka-section {
  max-width: var(--content-max);
  margin-inline: auto;
  padding-block: var(--space-12);
  border-top: 1px solid var(--border);
}

.imloka-section__title { margin-bottom: var(--space-6); }

/* --- discussion ----------------------------------------------------------- */

.imloka-discussion { max-width: 48rem; margin-inline: auto; }

.imloka-discussion__head {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--space-4);
  margin-bottom: var(--space-6);
}

.imloka-discussion__sort { display: flex; gap: var(--space-1); }

.imloka-composer { display: grid; gap: var(--space-3); margin-bottom: var(--space-8); }

.imloka-composer__actions {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-3);
}

.imloka-composer__draft-note {
  font-size: var(--type-meta);
  color: var(--text-muted);
}

/* --- data and graphics ---------------------------------------------------- */
/* "Data should feel like journalism, not business intelligence."
 *
 * Direct labelling, editorial captions, restrained gridlines, and a stated
 * source. No KPI cards, no dashboard chrome, no 3D, no decorative gauges. */

.imloka-graphic {
  margin-block: var(--space-10);
}

.imloka-graphic__title { margin-bottom: var(--space-1); }

.imloka-graphic__standfirst {
  margin-bottom: var(--space-5, 20px);
  color: var(--text-muted);
  font-size: var(--type-meta);
  line-height: 1.55;
}

.imloka-graphic__figure {
  padding: var(--space-5, 20px);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  background: var(--surface);
}

.imloka-graphic svg { display: block; width: 100%; height: auto; }

/* Series are distinguished by direct labels and structure, not hue alone:
   the chart must survive Graphite, forced colors, and grayscale. */
.imloka-graphic__bar { fill: var(--primary); }
.imloka-graphic__bar--muted { fill: var(--secondary); stroke: var(--primary); stroke-width: 1; }
.imloka-graphic__axis { stroke: var(--border); stroke-width: 1; }
.imloka-graphic__gridline { stroke: var(--border); stroke-width: 1; stroke-dasharray: 2 4; }
.imloka-graphic__label { fill: var(--text); font-size: 13px; font-weight: 500; }
.imloka-graphic__value { fill: var(--text-muted); font-size: 12px; font-variant-numeric: tabular-nums; }

.imloka-graphic__source {
  margin-top: var(--space-4);
  font-size: var(--type-meta);
  color: var(--text-muted);
  line-height: 1.5;
}

.imloka-graphic__toggle {
  display: flex;
  gap: var(--space-2);
  margin-bottom: var(--space-3);
}

/* The data table is a first-class equivalent, not a hidden fallback. */
.imloka-graphic__table[hidden] { display: none; }

@media (forced-colors: active) {
  .imloka-graphic__bar { fill: CanvasText; }
  .imloka-graphic__bar--muted { fill: Canvas; stroke: CanvasText; }
}
}

/* --- styles/patterns/author-account.css */
@layer patterns {
/* Patterns: author pages, account, special states, institutional pages.
 * §Author Pages, §Account and Personalization, §Special States. */

/* --- author masthead ------------------------------------------------------ */
/*   "Make the person clear. Make the work easy to find. Keep the profile from
 *    becoming the product."
 *
 * An editorial identity page, not a social profile. No follower count, no
 * verification checkmark, no engagement score. */

.imloka-author-masthead {
  display: grid;
  gap: var(--space-6);
  padding-block: var(--space-12) var(--space-10);
  border-bottom: 1px solid var(--border);
}

@media (min-width: 768px) {
  .imloka-author-masthead {
    grid-template-columns: minmax(0, 1fr) auto;
    align-items: start;
    gap: var(--space-10);
  }
  /* Portrait after the text in the source, beside it in the layout: the name
     and role should be what a screen reader reaches first. */
  .imloka-author-masthead__portrait { grid-column: 2; grid-row: 1; }
}

.imloka-author-masthead__body { display: grid; gap: var(--space-3); max-width: 46rem; }
.imloka-author-masthead__eyebrow { color: var(--text-muted); }
.imloka-author-masthead__role { color: var(--text-muted); font-size: var(--type-body-l); }
.imloka-author-masthead__bio { color: var(--text-muted); }

/* Editorial photography, square or portrait rectangle. Deliberately NOT a
   circle: a forced circle crops a considered portrait badly, and the spec
   says so outright. */
.imloka-author-masthead__portrait img {
  width: 200px;
  height: 240px;
  object-fit: cover;
  border-radius: var(--radius-sm);
  background: var(--surface-alt);
}

@media (min-width: 1024px) {
  .imloka-author-masthead__portrait img { width: 260px; height: 300px; }
}

/* No portrait produces a deliberate text-led masthead, not a grey silhouette
   or a broken image icon. */
.imloka-author-masthead--text-led .imloka-author-masthead__portrait { display: none; }

.imloka-author-masthead__actions {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-3);
  margin-top: var(--space-2);
}

.imloka-author-links {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2) var(--space-4);
  font-size: var(--type-meta);
}

/* --- account -------------------------------------------------------------- */
/*   "An account should deepen imloka without becoming a prerequisite for it."
 *
 * Deliberately absent, and absent by rule rather than by omission: reading
 * streaks, achievement badges, points, levels, leaderboards, karma, engagement
 * scores. Also no card-per-setting: settings are lists, not dashboards. */

.imloka-account {
  display: grid;
  gap: var(--space-8);
  padding-block: var(--space-10) var(--space-16);
}

@media (min-width: 1024px) {
  .imloka-account {
    grid-template-columns: minmax(0, 14rem) minmax(0, 1fr);
    gap: var(--space-12);
    align-items: start;
  }
}

.imloka-account__nav {
  display: grid;
  gap: var(--space-1);
  align-content: start;
}

@media (min-width: 1024px) {
  .imloka-account__nav { position: sticky; top: calc(var(--header-h-compact) + var(--space-6)); }
}

.imloka-account__nav-link {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-3);
  min-height: var(--target-min);
  padding-inline: var(--space-3);
  border-radius: var(--radius-xs);
  color: var(--text);
  font-size: var(--type-ui);
  text-decoration: none;
  transition: background-color var(--motion-fast) var(--ease-standard);
}

.imloka-account__nav-link:hover { background: var(--surface-alt); }

.imloka-account__nav-link[aria-current="page"] {
  background: var(--surface-alt);
  font-weight: var(--weight-semibold);
  box-shadow: inset 2px 0 0 var(--primary);
}

.imloka-account__nav-count { color: var(--text-muted); font-variant-numeric: tabular-nums; }

.imloka-account__main { display: grid; gap: var(--space-8); min-width: 0; }
.imloka-account__title { margin-bottom: var(--space-2); }
.imloka-account__lede { color: var(--text-muted); max-width: 56ch; }

/* Settings are lists with dividers, not a grid of cards. Cardifying settings
   is one of the drifts the spec names explicitly. */
.imloka-settings { display: grid; }

.imloka-settings__group { padding-block: var(--space-6); border-bottom: 1px solid var(--border); }
.imloka-settings__group:last-child { border-bottom: 0; }
.imloka-settings__legend { margin-bottom: var(--space-4); }

.imloka-settings__row {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-4);
  padding-block: var(--space-3);
}

.imloka-settings__row + .imloka-settings__row { border-top: 1px solid var(--border); }
.imloka-settings__label { font-size: var(--type-ui); }
.imloka-settings__note { display: block; font-size: var(--type-meta); color: var(--text-muted); max-width: 52ch; }

/* Destructive actions are discoverable, not hidden behind dark patterns;
   and visibly distinct from Sign out, which is not destructive. */
.imloka-settings__danger {
  margin-top: var(--space-6);
  padding: var(--space-5, 20px);
  border: 1px solid var(--danger);
  border-inline-start-width: 3px;
  border-radius: var(--radius-sm);
  background: var(--danger-surface);
}

.imloka-settings__danger .imloka-heading-s { color: var(--danger); }

/* --- special states ------------------------------------------------------- */
/*   "When imloka cannot give the reader what they asked for, it should still
 *    give them clarity, continuity, and somewhere sensible to go next."
 *
 * Header, Menu, Search and Footer survive wherever technically possible, so a
 * failure never strands the reader on an orphaned page. */

.imloka-state {
  display: grid;
  gap: var(--space-4);
  align-content: center;
  max-width: 44rem;
  min-height: 46vh;
  padding-block: var(--space-16) var(--space-20);
}

.imloka-state__label { color: var(--text-muted); }
.imloka-state__body { color: var(--text-muted); }

.imloka-state__actions {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-3);
  margin-top: var(--space-4);
}

/* Danger stays restrained. A saturated full-page error is itself a kind of
   shouting, and the spec rules it out. */
.imloka-state--unavailable .imloka-state__label,
.imloka-state--retracted .imloka-state__label { color: var(--danger); }

/* A section that failed inside an otherwise healthy page. The failure is
   contained: everything that loaded stays on screen. */
.imloka-section-state {
  display: grid;
  gap: var(--space-3);
  justify-items: start;
  padding: var(--space-6);
  border: 1px dashed var(--border);
  border-radius: var(--radius-sm);
  background: var(--surface-alt);
}

/* --- institutional pages -------------------------------------------------- */

.imloka-doc {
  max-width: var(--reading-max);
  margin-inline: auto;
  padding-block: var(--space-12) var(--space-16);
}

.imloka-doc__title { margin-bottom: var(--space-3); }
.imloka-doc__lede { color: var(--text-muted); margin-bottom: var(--space-10); font-size: var(--type-body-l); }
/* The lede carries the gap between the opening block and the body copy, so a
   doc page without one would otherwise run its title into the first paragraph
   on the title's own --space-3. Not a stopgap: brand statements are not
   carried across editions (IMLOKA-HI-06), so hi/about.html has no lede where
   the English one does, and further languages will land the same way. Set on
   the paragraph rather than the title because these margins collapse: the
   larger of the two wins, which makes this a predictable 32px either way. */
.imloka-doc__title + p:not(.imloka-doc__lede) { margin-top: var(--space-8); }
/* scroll-margin-top so a direct #grievance link (footer) lands the heading
   clear of the sticky Header, same offset article.css uses for its own
   in-page anchors. */
.imloka-doc h2 { margin-top: 2.2em; margin-bottom: 0.6em; scroll-margin-top: calc(var(--header-h-compact) + var(--space-6)); }

/* The newsletter page nests content.css's `.imloka-newsletter` card inside a
   .imloka-doc article; this layer loads after @layer components (see the
   @layer order in tools/build.mjs), so a components-layer override could
   never win here regardless of specificity: the reset has to live in this
   same layer, next to the rule it's countering. Without it, the card's own
   heading gets the 2.2em meant for headings that separate paragraphs,
   stacked on top of the card's own padding. */
.imloka-doc .imloka-newsletter h2 { margin-top: 0; }
.imloka-doc p + p { margin-top: 1.4em; }
.imloka-doc ul { margin-top: 1em; padding-inline-start: 1.3em; list-style: disc; }
.imloka-doc li + li { margin-top: 0.5em; }
.imloka-doc a { color: var(--link); text-decoration: underline; text-underline-offset: 0.18em; }

/* --- contact: copy + form card --------------------------------------------- */
/* One column until the form has room beside the copy rather than under it:
   same "the form decides, not the viewport" reasoning as the newsletter
   module's own container query (content.css). */

.imloka-contact {
  display: grid;
  gap: var(--space-10);
  padding-block: var(--space-12) var(--space-16);
}

.imloka-contact__copy.imloka-doc { max-width: none; margin-inline: 0; padding-block: 0; }

@media (min-width: 960px) {
  .imloka-contact { grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); align-items: start; }
}

.imloka-contact__form-card {
  padding: var(--space-8);
  border-radius: var(--radius-sm);
  background: var(--surface-alt);
}

.imloka-contact__form-card > h2 { margin-bottom: var(--space-6); }
.imloka-contact__form { display: grid; gap: var(--space-5); }
.imloka-contact__status { font-size: var(--type-meta); color: var(--text-muted); }
}

/* --- styles/patterns/cookie-banner.css */
@layer patterns {
/* Patterns: cookie consent banner. GDPR / Google AdSense EU User Consent
 * Policy: non-essential cookies stay off until a reader actively consents,
 * so this has to gate real behaviour (src/scripts/cookie-consent.js), not
 * just disclose it. See src/partials/shell/cookie-banner.html. */

.imloka-cookie-banner {
  position: fixed;
  inset-inline: 0;
  inset-block-end: 0;
  z-index: var(--z-toast);
  background: var(--surface);
  border-top: 1px solid var(--border);
}

/* A full-screen mobile Menu/Search already covers this; on desktop it would
 * otherwise float on top of the mega-menu it should defer to. */
[data-layer] .imloka-cookie-banner { display: none; }

.imloka-cookie-banner__inner {
  display: grid;
  gap: var(--space-4);
  padding-block: var(--space-6);
  max-height: 80vh;
  overflow-y: auto;
}

.imloka-cookie-banner__summary { display: grid; gap: var(--space-4); }

.imloka-cookie-banner__summary .imloka-body { max-width: 68ch; margin: 0; }
.imloka-cookie-banner__summary .imloka-body a {
  color: var(--link);
  text-decoration: underline;
  text-underline-offset: 0.18em;
}

.imloka-cookie-banner__actions {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-3);
}

.imloka-cookie-banner__manage { display: grid; gap: var(--space-2); }

@media (min-width: 700px) {
  .imloka-cookie-banner__summary {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--space-6);
  }

  .imloka-cookie-banner__actions { flex: 0 0 auto; }
}
}

/* --- styles/patterns/layers.css */
@layer patterns {
/* Patterns: the two global layers: Menu and Search.
 *
 * Both share a structure: a dismiss surface plus a panel. They are mutually
 * exclusive and share a z-token, because only one can ever be open.
 *
 * Progressive enhancement: `:target` opens a layer with no JS at all. The
 * [data-state] attribute is what the layer manager drives once JS is running.
 * Both selectors are listed everywhere so neither path is second-class. */

.imloka-menu-layer,
.imloka-search-layer {
  position: fixed;
  inset: 0;
  z-index: var(--z-menu);
  display: grid;
  grid-template-rows: auto 1fr;
  visibility: hidden;
  pointer-events: none;
}

/* Desktop: the mega-menu opens BELOW the header, which stays visible and
   operable: its trigger is how the reader closes the layer again, and a
   header buried under the panel would strand them. Mobile keeps inset:0
   because the layer is deliberately full-screen there. */
@media (min-width: 1024px) {
  .imloka-menu-layer,
  .imloka-search-layer { top: var(--header-h-compact); }
}

.imloka-menu-layer[data-state="open"],
.imloka-search-layer[data-state="open"],
.imloka-menu-layer:target,
.imloka-search-layer:target {
  visibility: visible;
  pointer-events: auto;
}

/* With JS the element is also `hidden`; :target must beat that for the no-JS
   path to work at all. */
.imloka-menu-layer:target,
.imloka-search-layer:target { display: grid !important; }

.imloka-menu-layer__dismiss,
.imloka-search-layer__dismiss {
  position: absolute;
  inset: 0;
  background: var(--image-overlay);
  opacity: 0;
  transition: opacity var(--motion-medium) var(--ease-standard);
}

.imloka-menu-layer[data-state="open"] .imloka-menu-layer__dismiss,
.imloka-search-layer[data-state="open"] .imloka-search-layer__dismiss,
.imloka-menu-layer:target .imloka-menu-layer__dismiss,
.imloka-search-layer:target .imloka-search-layer__dismiss { opacity: 1; }

.imloka-menu-layer__panel,
.imloka-search-layer__panel {
  position: relative;
  grid-row: 1;
  overflow-y: auto;
  overscroll-behavior: contain;
  max-height: 100dvh;
  background: var(--surface);
  border-bottom: 1px solid var(--border);
  /* Opening is slightly slower than closing, per the motion contract. */
  transform: translateY(-8px);
  opacity: 0;
  transition:
    transform var(--motion-large) var(--ease-enter),
    opacity var(--motion-medium) var(--ease-standard);
}

.imloka-menu-layer[data-state="open"] .imloka-menu-layer__panel,
.imloka-search-layer[data-state="open"] .imloka-search-layer__panel,
.imloka-menu-layer:target .imloka-menu-layer__panel,
.imloka-search-layer:target .imloka-search-layer__panel {
  transform: translateY(0);
  opacity: 1;
}

.imloka-menu-layer__inner,
.imloka-search-layer__inner {
  position: relative;
  padding-block: var(--space-8) var(--space-12);
}

/* Mobile: a full-screen layer, deliberately recomposed rather than a squeezed
   mega-menu. Safe-area aware, and sized with dvh so browser chrome that grows
   and shrinks does not clip the close control. */
@media (max-width: 1023px) {
  .imloka-menu-layer__panel,
  .imloka-search-layer__panel {
    grid-row: 1 / -1;
    min-height: 100dvh;
    max-height: 100dvh;
    padding-bottom: env(safe-area-inset-bottom);
    transform: translateY(-100%);
    transition:
      transform var(--motion-large) var(--ease-enter),
      opacity var(--motion-fast) var(--ease-standard);
  }
}

/* A flow-positioned head row rather than an absolutely-positioned close button:
   an absolute one sat flush against the viewport edge on mobile and its focus
   ring was clipped, which is a focus-visibility failure, not a cosmetic one. */
.imloka-menu-layer__head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-4);
  margin-bottom: var(--space-8);
}

/* On desktop the mega-menu sits below a visible header whose Menu control
   toggles it, so the in-panel head is redundant. Esc works in both. */
@media (min-width: 1024px) {
  .imloka-menu-layer__head { display: none; }
}

/* --- menu ----------------------------------------------------------------- */

.imloka-menu-layer__groups {
  display: grid;
  gap: var(--space-8);
  grid-template-columns: minmax(0, 1fr);
  padding-bottom: var(--space-10);
}

@media (min-width: 768px) {
  .imloka-menu-layer__groups { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}

@media (min-width: 1024px) {
  /* Four groups plus one editorial card: the spec allows at most one or two
     such cards, so the menu stays navigation-first. */
  .imloka-menu-layer__groups {
    grid-template-columns: repeat(4, minmax(0, 1fr)) minmax(0, 1.2fr);
    gap: var(--space-6);
  }
}

/* Alternating tone across the mega-menu's columns, so each group reads as its
   own panel: same semantic tone vocabulary as everywhere else (§tone,
   mosaic.css: neutral | soft | primary), not a one-off colour. Padding is
   offset by an equal negative margin (a "bleed") so a toned group's title and
   list still line up with its neutral neighbours; the bleed is capped at
   --space-3 so two independently-bled panels never touch even at the
   tightest column gap (--space-6 at desktop: see .imloka-menu-layer__groups). */
.imloka-menu-group[data-tone="soft"] {
  background: var(--surface-alt);
  border-radius: var(--radius-sm);
  padding: var(--space-3);
  margin: calc(var(--space-3) * -1);
}

/* Hovering a link would otherwise highlight to the same colour as the panel
   it already sits on: inverted here so the hover state stays visible on
   both tones, same principle as data-tone="primary" inverting the tile's own
   text colour in mosaic.css. */
.imloka-menu-group[data-tone="soft"] .imloka-menu-group__link:hover { background: var(--surface); }

.imloka-menu-group__title {
  color: var(--text-muted);
  margin-bottom: var(--space-4);
}

.imloka-menu-group__title-link {
  color: inherit;
  text-decoration: none;
}

.imloka-menu-group__title-link:hover,
.imloka-menu-group__title-link:focus-visible {
  text-decoration: underline;
}

.imloka-menu-group__list { display: grid; gap: var(--space-1); }

.imloka-menu-group__link {
  display: grid;
  gap: 2px;
  min-height: var(--target-min);
  align-content: center;
  padding: var(--space-2) var(--space-3);
  margin-inline: calc(var(--space-3) * -1);
  border-radius: var(--radius-xs);
  color: var(--text);
  text-decoration: none;
  transition:
    background-color var(--motion-fast) var(--ease-standard),
    color var(--motion-fast) var(--ease-standard);
}

.imloka-menu-group__link:hover { background: var(--surface-alt); color: var(--primary); }

.imloka-menu-group__label {
  font-size: var(--type-ui);
  font-weight: var(--weight-medium);
}

.imloka-menu-group__note {
  font-size: var(--type-meta);
  color: var(--text-muted);
}

.imloka-menu-feature {
  display: grid;
  gap: var(--space-2);
  align-content: start;
  align-self: start;
  padding: var(--space-6);
  border-radius: var(--radius-sm);
  background: var(--surface);
  color: var(--text);
  text-decoration: none;
  transition: background-color var(--motion-fast) var(--ease-standard);
}

.imloka-menu-feature:hover { background: var(--secondary); }
.imloka-menu-feature__kicker { color: var(--text-muted); }
.imloka-menu-feature__title { color: var(--text); }
.imloka-menu-feature:hover .imloka-menu-feature__title { text-decoration: underline; text-underline-offset: 0.16em; }

.imloka-menu-utilities {
  display: grid;
  gap: var(--space-8);
  padding-top: var(--space-8);
  border-top: 1px solid var(--border);
}

@media (min-width: 1024px) {
  .imloka-menu-utilities {
    grid-template-columns: minmax(0, 1fr) auto;
    align-items: start;
  }
}

.imloka-menu-utilities__links {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2) var(--space-6);
}

.imloka-menu-utilities__link {
  display: inline-flex;
  align-items: center;
  min-height: var(--target-min);
  color: var(--text);
  font-size: var(--type-ui);
  text-decoration: none;
}

.imloka-menu-utilities__link:hover { color: var(--primary); text-decoration: underline; text-underline-offset: 0.18em; }

.imloka-menu-controls {
  display: grid;
  gap: var(--space-6);
}

@media (min-width: 640px) {
  .imloka-menu-controls { grid-auto-flow: column; grid-auto-columns: max-content; gap: var(--space-8); }
}

.imloka-control-group { display: grid; gap: var(--space-2); }
.imloka-control-group__label { color: var(--text-muted); }

/* --- search --------------------------------------------------------------- */

.imloka-search-layer__head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-4);
  margin-bottom: var(--space-6);
}

.imloka-search-form {
  display: flex;
  gap: var(--space-3);
  align-items: stretch;
}

.imloka-search-form__field {
  position: relative;
  display: flex;
  align-items: center;
  flex: 1 1 auto;
  min-width: 0;
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  background: var(--surface);
  transition: border-color var(--motion-fast) var(--ease-standard);
}

.imloka-search-form__field:focus-within { border-color: var(--primary); }

.imloka-search-form__icon {
  display: grid;
  place-items: center;
  padding-inline: var(--space-3);
  color: var(--text-muted);
}

.imloka-search-form__input {
  flex: 1 1 auto;
  min-width: 0;
  min-height: 48px;
  padding-inline: 0 var(--space-3);
  border: 0;
  background: none;
  /* 16px minimum prevents iOS zooming the page on focus. */
  font-size: max(16px, var(--type-body));
  color: var(--text);
}

.imloka-search-form__input:focus { outline: none; }
.imloka-search-form__input::placeholder { color: var(--text-muted); }
.imloka-search-form__input::-webkit-search-cancel-button { display: none; }

.imloka-search-layer__body { margin-top: var(--space-8); }

/* Only the panel matching the current state is rendered. */
.imloka-search-panel { display: none; }
[data-search-state="idle"]    .imloka-search-panel[data-when="idle"],
[data-search-state="typing"]  .imloka-search-panel[data-when="idle"],
[data-search-state="loading"] .imloka-search-panel[data-when="loading"],
[data-search-state="results"] .imloka-search-panel[data-when="results"],
[data-search-state="zero"]    .imloka-search-panel[data-when="zero"],
[data-search-state="partial"] .imloka-search-panel[data-when="partial"],
[data-search-state="failure"] .imloka-search-panel[data-when="failure"] { display: block; }

.imloka-search-section + .imloka-search-section { margin-top: var(--space-8); }
.imloka-search-section__title { color: var(--text-muted); margin-bottom: var(--space-3); }

.imloka-search-results { display: grid; gap: var(--space-1); }

.imloka-search-result {
  display: grid;
  gap: 2px;
  padding: var(--space-3);
  margin-inline: calc(var(--space-3) * -1);
  border-radius: var(--radius-xs);
  color: var(--text);
  text-decoration: none;
  transition: background-color var(--motion-fast) var(--ease-standard);
}

.imloka-search-result:hover { background: var(--surface-alt); }
.imloka-search-result:hover .imloka-search-result__title { color: var(--primary); }

.imloka-search-result--top {
  background: var(--surface-alt);
  border-inline-start: 3px solid var(--primary);
}

.imloka-search-result mark {
  background: var(--secondary);
  color: var(--text);
  border-radius: 2px;
}

.imloka-search-more { margin-top: var(--space-8); }

/* Search results use a compact list treatment, never homepage mosaic geometry. */
.imloka-skeleton-row {
  display: grid;
  gap: var(--space-2);
  padding: var(--space-3) 0;
}

.imloka-skeleton {
  display: block;
  height: 12px;
  border-radius: var(--radius-xs);
  background: var(--surface-alt);
  animation: imloka-skeleton-pulse var(--motion-skeleton) var(--ease-linear) infinite;
}

@keyframes imloka-skeleton-pulse {
  0%, 100% { opacity: 1; }
  50% { opacity: .55; }
}

@media (prefers-reduced-motion: reduce) {
  /* The skeleton still communicates "loading" by existing; only the pulse goes. */
  .imloka-skeleton { animation: none; }
}

/* --- header state --------------------------------------------------------- */

.imloka-header[data-state="compact"] .imloka-header__inner { height: var(--header-h-compact); }

.imloka-header[data-state="hidden-reading"] {
  transform: translateY(-100%);
}

.imloka-header {
  transition: transform var(--motion-base) var(--ease-standard);
}

/* While a layer is open the header must stay put: its trigger lives there. */
:root[data-layer] .imloka-header { transform: none; }

/* A second close at the foot of the mobile layer.
 *
 * Same reasoning as the head row above, applied to the other end: the panel
 * scrolls, and a control that has scrolled out of reach is not available. This
 * one is labelled rather than icon-only: at the bottom of a long list, a bare
 * × has no context to borrow, whereas the head's × sits beside the word "Menu".
 *
 * The layer manager binds every [data-layer-close] in the panel, and skips ones
 * that are not visible when it computes the focus order, so the desktop-hidden
 * copy neither steals initial focus nor appears in the tab ring. */
.imloka-menu-layer__foot {
  margin-top: var(--space-10);
  padding-top: var(--space-6);
  border-top: 1px solid var(--border);
}

.imloka-menu-layer__foot-close {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-2);
  width: 100%;
  min-height: var(--target-min);
  padding: var(--space-3);
  background: none;
  border: 1px solid var(--border);
  border-radius: var(--radius-xs);
  color: var(--text);
  font-size: var(--type-ui);
  font-weight: var(--weight-medium);
  cursor: pointer;
  transition:
    background-color var(--motion-fast) var(--ease-standard),
    border-color var(--motion-fast) var(--ease-standard);
}

.imloka-menu-layer__foot-close:hover {
  background: var(--surface-alt);
  border-color: var(--primary);
}

/* Desktop already has an always-visible Menu control in the header. */
@media (min-width: 1024px) {
  .imloka-menu-layer__foot { display: none; }
}
}

/* --- styles/patterns/mosaic.css */
@layer patterns {
/* Patterns: the editorial mosaic. §Editorial Tile Geometry v2.1.
 *
 *   "A tile is geometry. A module is meaning."
 *
 * The labels 1x1, 2x1, 1x2 and 2x2 describe MODULAR SPAN AND COMPOSITIONAL
 * ROLE: not a mandated physical aspect ratio. There is deliberately no
 * `aspect-ratio` bound to any [data-tile] selector, and tools/lint-css.mjs
 * fails the build if one appears. The reason is concrete: an English and a
 * Hindi headline of the same story occupy different heights, and a locked
 * ratio would force one of them to crop or shrink.
 *
 *   1x1  compact standard unit      local rhythm and utility
 *   2x1  horizontal secondary       movement across the composition
 *   1x2  vertical anchor            punctuation: NOT a rotated 2x1
 *   2x2  dominant editorial feature the primary hierarchy
 *
 * Canonical desktop rhythm: 2x2 + 1x2 + 2x1 + 1x1.
 */

.imloka-mosaic {
  display: grid;
  grid-template-columns: repeat(var(--grid-columns), minmax(0, 1fr));
  /* Rows size to content. A fixed row height would reintroduce the aspect-ratio
     problem through the back door. */
  grid-auto-rows: minmax(180px, auto);
  gap: var(--grid-gap);
}

.imloka-tile {
  position: relative;
  display: flex;
  flex-direction: column;
  min-width: 0;
  overflow: hidden;
  border-radius: var(--radius-xs);
  background: var(--surface);
  /* The tile is a container, so its media can respond to the width the tile
     actually got rather than to the viewport. The spec asks for exactly this:
     "prefer intrinsic sizing, minmax(), clamp(), container queries, and
     component-level recomposition"; and it is what lets the photograph adapt
     without any rule ever consulting [data-tile]. */
  container-type: inline-size;
}

/* --- mobile: two editorial columns --------------------------------------- */
/* Recomposition, not scaling. Hierarchy and intent survive; literal orientation
   does not. DOM order already follows editorial priority, so the mobile stack
   reads in the right order without any reordering (C5). */

.imloka-tile[data-tile] { grid-column: span 2; }

/* Only 1x1 units pair, and only where they stay readable. */
.imloka-tile[data-tile="1x1"] { grid-column: span 1; }

@media (max-width: 479px) {
  /* At the narrowest widths even paired 1x1 tiles stop being readable. */
  .imloka-tile[data-tile="1x1"] { grid-column: span 2; }
}

/* A compact unit with no partner spans instead of sitting beside a hole. The
   renderer computes this from queue order (registry.markOrphans) because a
   stylesheet cannot see which tile follows which. */
.imloka-tile[data-tile="1x1"][data-orphan="true"] { grid-column: span 2; }

/* --- tablet: six columns -------------------------------------------------- */

@media (min-width: 768px) {
  .imloka-tile[data-tile="2x2"] { grid-column: span 6; grid-row: span 2; }
  /* The vertical anchor recomposes horizontally at six columns. It keeps its
     prominence (full bleed across the grid) without leaving a three-column
     void that only a 1x1 could fill. Mobile is recomposition, not scaling, and
     that applies to the tablet range too. */
  .imloka-tile[data-tile="1x2"] { grid-column: span 6; grid-row: span 1; }
  .imloka-tile[data-tile="2x1"] { grid-column: span 6; }
  .imloka-tile[data-tile="1x1"] { grid-column: span 3; }
  .imloka-tile[data-tile="1x1"][data-orphan="true"] { grid-column: span 6; }
}

/* --- desktop: twelve columns ---------------------------------------------- */

@media (min-width: 1024px) {
  .imloka-tile[data-tile="2x2"] { grid-column: span 6; grid-row: span 2; }
  .imloka-tile[data-tile="1x2"] { grid-column: span 3; grid-row: span 2; }
  .imloka-tile[data-tile="2x1"] { grid-column: span 6; grid-row: span 1; }
  .imloka-tile[data-tile="1x1"] { grid-column: span 3; grid-row: span 1; }

  /* An orphan is a SIX-column problem, and this grid has twelve.
     The tablet rule above is more specific than the plain 1x1 rule here, so
     without this line it leaked into the desktop grid and a partnerless 1x1
     claimed half the row. Where only three columns remained it could not fit,
     wrapped, and left a 3x2 hole in the middle of the composition - which is
     the one thing the orphan rule exists to prevent.
     At twelve columns four 1x1s fill a row, so a lone one leaves no hole and
     needs no special treatment. */
  .imloka-tile[data-tile="1x1"][data-orphan="true"] { grid-column: span 3; }
}

/* --- tone ----------------------------------------------------------------- */
/* CMS tone control is semantic (neutral | soft | primary), never a colour
   picker. Saturated surfaces stay limited across a composition. */

.imloka-tile[data-tone="soft"] { background: var(--surface-alt); }

.imloka-tile[data-tone="primary"] {
  background: var(--primary);
  color: var(--text-on-primary);
}

.imloka-tile[data-tone="primary"] .imloka-tile__kicker,
.imloka-tile[data-tone="primary"] .imloka-tile__meta { color: inherit; opacity: .85; }
.imloka-tile[data-tone="primary"] .imloka-tile__title a { color: inherit; }

.imloka-tile[data-tone="secondary"] { background: var(--secondary); }

/* --- tile body ------------------------------------------------------------ */

.imloka-tile__body {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  flex: 1 1 auto;
  padding: var(--space-5, 20px);
}

@media (min-width: 1024px) {
  .imloka-tile[data-tile="2x2"] .imloka-tile__body { padding: var(--space-8); gap: var(--space-3); }
  .imloka-tile[data-tile="1x2"] .imloka-tile__body { padding: var(--space-6); }
}

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

.imloka-tile__title {
  font-weight: var(--weight-semibold);
  letter-spacing: var(--track-heading);
}

.imloka-tile__title a {
  color: var(--text);
  text-decoration: none;
}

/* Whole-tile click via a stretched link. Allowed only with correct link
   semantics: a real anchor, a real accessible name, and a focus ring that
   outlines the tile rather than a zero-size pseudo-element. */
.imloka-tile__title a::after {
  content: "";
  position: absolute;
  inset: 0;
}

.imloka-tile:has(a:focus-visible) {
  outline: 2px solid var(--focus);
  outline-offset: 2px;
}

.imloka-tile__title a:focus-visible { outline: none; }

.imloka-tile__deck {
  color: var(--text-muted);
  font-size: var(--type-meta);
  line-height: 1.55;
}

.imloka-tile[data-tile="2x2"] .imloka-tile__deck {
  font-size: var(--type-body);
  line-height: 1.6;
}

.imloka-tile__meta {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-1) var(--space-2);
  margin-top: auto;
  padding-top: var(--space-3);
  font-size: var(--type-meta);
  color: var(--text-muted);
}

/* Title scale by tile role. This is the tile-pressure system's first move:
   wrap, then step down a type role, then recompose: truncate only last. */
.imloka-tile[data-tile="1x1"] .imloka-tile__title { font-size: var(--type-heading-s); line-height: var(--type-heading-s-lh); }
.imloka-tile[data-tile="2x1"] .imloka-tile__title { font-size: var(--type-heading-m); line-height: var(--type-heading-m-lh); }
.imloka-tile[data-tile="1x2"] .imloka-tile__title { font-size: var(--type-heading-m); line-height: var(--type-heading-m-lh); }
.imloka-tile[data-tile="2x2"] .imloka-tile__title { font-size: var(--type-display-m); line-height: var(--type-display-m-lh); letter-spacing: var(--track-display); }

/* A very long headline steps down one role rather than overflowing or
   clipping. Set by the renderer, not guessed by CSS. */
.imloka-tile[data-pressure="high"][data-tile="2x2"] .imloka-tile__title { font-size: var(--type-heading-l); line-height: var(--type-heading-l-lh); }
.imloka-tile[data-pressure="high"][data-tile="2x1"] .imloka-tile__title,
.imloka-tile[data-pressure="high"][data-tile="1x2"] .imloka-tile__title { font-size: var(--type-heading-s); line-height: var(--type-heading-s-lh); }

/* --- media ---------------------------------------------------------------- */

.imloka-tile__media {
  position: relative;
  display: block;
  overflow: hidden;
  background: var(--surface-alt);
}

/* Media proportions follow the tile's rendered WIDTH, not its role.
 *
 * Before this, media had no ratio and the image's natural proportions set the
 * tile's height. Two consequences, both reported: a 2x1 at 670px wide grew a
 * 446px photograph and became 777px tall (taller than the two-row tiles beside
 * it), and two tiles sharing a row showed photographs of different heights,
 * because the media absorbed whatever slack the longer headline next to it left
 * over.
 *
 * A wider tile therefore gets a wider crop rather than a taller one. Bound to
 * container width, so it is a property of the space the tile occupies, not of
 * the notation: `2x1` still means "horizontal secondary anchor", not "2:1". */
.imloka-tile__media { aspect-ratio: 3 / 2; }

@container (min-width: 420px) {
  .imloka-tile__media { aspect-ratio: 2 / 1; }
}

@container (min-width: 620px) {
  .imloka-tile__media { aspect-ratio: 3 / 1; }
}

.imloka-tile__media img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  /* Focal point comes from the CMS as two 0-1 values, passed through as custom
     properties: the single sanctioned use of inline style. */
  object-position: calc(var(--focal-x, .5) * 100%) calc(var(--focal-y, .5) * 100%);
  transition: transform var(--motion-medium) var(--ease-standard);
}

/* Hover uses at most one or two signals. A large lift plus shadow plus scale
   plus arrow motion all at once is exactly what the spec forbids. */
.imloka-tile--image-led:hover .imloka-tile__media img { transform: scale(1.015); }
.imloka-tile:hover .imloka-tile__title a { text-decoration: underline; text-underline-offset: 0.16em; }

@media (prefers-reduced-motion: reduce) {
  .imloka-tile--image-led:hover .imloka-tile__media img { transform: none; }
}

/* Image-led tiles put the image above the text.
 *
 * The media no longer absorbs the row's slack: that is a deliberate reversal.
 * Letting the photograph grow meant its height depended on how long the
 * headline in the tile NEXT to it happened to be, so two photographs side by
 * side never matched. A band of quiet surface under a short headline is calmer
 * than a photograph that changes size for reasons the reader cannot see.
 *
 * The slack goes to the body instead, which simply has room to breathe. */
.imloka-tile--image-led .imloka-tile__media { flex: 0 0 auto; min-height: 140px; }
.imloka-tile--image-led .imloka-tile__body { flex: 1 1 auto; }

/* Except when being tall IS the job. A 1x2 is a vertical anchor and a 2x2 the
   dominant feature; for those the photograph takes the extra row, which is the
   compositional point of the role rather than a ratio imposed on it. The body
   stays pinned to its own height so the slack goes to the picture, not into a
   gap between the deck and the byline. */
.imloka-tile--tall.imloka-tile--image-led .imloka-tile__media {
  flex: 1 1 auto;
  aspect-ratio: auto;
}

.imloka-tile--tall.imloka-tile--image-led .imloka-tile__body { flex: 0 0 auto; }

/* A tall role (1x2/2x2) is only reliably tall relative to its OWN row unless
 * something guarantees it. Grid auto-rows sizes each row to whatever content
 * shares it, so a tall tile's actual height has repeatedly ended up set by an
 * unrelated short tile several rows down the same column track: first the
 * homepage hero, then the Newsletter tile, then this. A floor here closes the
 * gap once, for every tall tile on every page, instead of chasing it row by
 * row as content changes. Sized above the tallest short-role tile measured on
 * the homepage queue (465px) with headroom for future copy; raise it if a
 * short tile ever needs to grow past that. */
.imloka-tile--tall { min-height: 500px; }

/* Immersive tiles lay text over the image, which is where --image-overlay
   earns its place: text over difficult photography must stay readable. */
.imloka-tile--immersive { color: var(--text-on-primary); }
/* An immersive tile's media is the tile: absolutely positioned to fill it, with
   the text laid over. It must not take a ratio: inset:0 already determines
   both dimensions, and leaving the container-query ratio in place cropped the
   photograph to a band across the top with empty surface beneath it. */
.imloka-tile--immersive .imloka-tile__media {
  position: absolute;
  inset: 0;
  aspect-ratio: auto;
}

.imloka-tile--immersive .imloka-tile__media::after {
  content: "";
  position: absolute;
  inset: 0;
  background:
    linear-gradient(to top, var(--image-overlay) 0%, transparent 62%),
    var(--image-overlay);
}

.imloka-tile--immersive .imloka-tile__body {
  position: relative;
  justify-content: flex-end;
  min-height: 320px;
  color: var(--text-on-image);
}

.imloka-tile--immersive .imloka-tile__title a,
.imloka-tile--immersive .imloka-tile__kicker,
.imloka-tile--immersive .imloka-tile__deck,
.imloka-tile--immersive .imloka-tile__meta { color: var(--text-on-image); }

@media (min-width: 1024px) {
  .imloka-tile--immersive[data-tile="2x2"] .imloka-tile__body { min-height: 420px; }
}

/* --- text-only ------------------------------------------------------------ */
/* A text-only story has no photograph, so on the home/account mosaic (never on
   the story page itself — story-tile.html is not used there) its reserved
   media slot shows the app mark instead of standing empty. Sized and laid out
   the same way an image-led tile's media is. */

.imloka-tile--text-only .imloka-tile__media { flex: 0 0 auto; min-height: 140px; }
.imloka-tile--text-only .imloka-tile__body { flex: 1 1 auto; }

.imloka-tile--tall.imloka-tile--text-only .imloka-tile__media {
  flex: 1 1 auto;
  aspect-ratio: auto;
}

.imloka-tile--tall.imloka-tile--text-only .imloka-tile__body { flex: 0 0 auto; }

.imloka-tile__media--icon {
  display: flex;
  align-items: center;
  justify-content: center;
}

.imloka-tile__media--icon .imloka-mark {
  width: 22%;
  min-width: 28px;
  max-width: 64px;
  height: auto;
  color: var(--text-muted);
}

/* In a text-only tile the byline should sit with the headline rather than being
   pushed to the far bottom by `margin-top: auto`: a quiet tile is a
   deliberate pause in the pacing, not an empty container with a caption. */
.imloka-tile--text-only .imloka-tile__meta { margin-top: var(--space-2); }

/* --- empty -------------------------------------------------------------- */
/* A reserved slot with nothing assigned. Dashed border and centred meta text
   mark it visibly as "nothing here yet" rather than a broken tile: the same
   visual vocabulary the house-promo ad state already uses for "not the real
   thing filling this space". */
.imloka-tile--empty {
  border: 1px dashed var(--border);
  background: none;
}

.imloka-tile--empty .imloka-tile__body {
  align-items: center;
  justify-content: center;
  text-align: center;
}
}

/* --- styles/patterns/shell.css */
@layer patterns {
/* Patterns: global page shell: header and footer.
 * §Header and Global Navigation, §Footer System. */

.imloka-visually-hidden {
  position: absolute;
  width: 1px; height: 1px;
  padding: 0; margin: -1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

/* ------------------------------------------------------------------ header */
/* "Calm Header, expressive canvas, powerful Menu."
 * Opaque --surface with a subtle border. No glassmorphism, no hero-overlay
 * transparency: both are explicitly excluded by the spec. */

.imloka-header {
  position: sticky;
  top: 0;
  z-index: var(--z-header);
  background: var(--surface);
  border-bottom: 1px solid var(--border);
}

.imloka-header__inner {
  display: flex;
  align-items: center;
  gap: var(--space-6);
  height: var(--header-h);
  transition: height var(--motion-medium) var(--ease-standard);
}

.imloka-header__brand {
  display: flex;
  align-items: center;
  flex: 0 0 auto;
  /* Visual size and hit area are different things. The wordmark stays at its
     24-30px optical height; the link around it carries the minimum target. */
  min-height: var(--target-min);
  color: var(--text);
  text-decoration: none;
}

/* The spec asks for 24–30px desktop / 22–26px mobile *visual* height. Because
 * the wordmark is cropped tight to its ink, a CSS height is that visual
 * height: no padded box to compensate for. */
.imloka-wordmark {
  height: 23px;
  width: auto;
  display: block;
}

@media (min-width: 1024px) {
  .imloka-wordmark { height: 27px; }
}

.imloka-header__nav {
  flex: 1 1 auto;
  min-width: 0;
}

/* Primary navigation never wraps (§Primary Navigation). Lower-priority
 * destinations move into the Menu instead. */
.imloka-header__nav-list {
  display: flex;
  align-items: center;
  gap: var(--space-6);
  flex-wrap: nowrap;
}

.imloka-header__nav-link {
  position: relative;
  display: inline-flex;
  align-items: center;
  min-height: var(--target-min);
  font-size: var(--type-ui);
  font-weight: var(--weight-medium);
  color: var(--text);
  text-decoration: none;
  white-space: nowrap;
  transition: color var(--motion-fast) var(--ease-standard);
}

/* Devanagari at the shared --type-ui size reads visibly smaller than Latin
   next to it (thinner strokes, no ascender/descender extension), so the
   primary nav gets a small Hindi-only bump rather than raising --type-ui
   itself, which would also resize every button/form/badge sharing that
   token. */
.imloka-header__nav-link:lang(hi) {
  font-size: calc(var(--type-ui) + 1px);
}

.imloka-header__nav-link:hover { color: var(--primary); }

/* Current state is weight + a 2px marker, never colour alone (C11). */
.imloka-header__nav-link[aria-current="page"] {
  font-weight: var(--weight-semibold);
  color: var(--primary);
}

.imloka-header__nav-link[aria-current="page"]::after {
  content: "";
  position: absolute;
  inset-inline: 0;
  bottom: 14px;
  height: 2px;
  background: currentColor;
}

.imloka-header__actions {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  flex: 0 0 auto;
  margin-inline-start: auto;
}

.imloka-header__search-icon,
.imloka-header__menu-icon,
.imloka-header__lang-icon { display: block; }

/* Search, Menu and the language switch share one quiet, bordered button
   treatment: an icon leading a label, both always visible. */
.imloka-header__search-trigger,
.imloka-header__menu-trigger,
.imloka-header__lang-trigger {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  min-height: var(--target-min);
  padding-inline: var(--space-3);
  background: none;
  border: 1px solid var(--border);
  border-radius: var(--radius-xs);
  color: var(--text);
  font-size: var(--type-ui);
  font-weight: var(--weight-medium);
  text-decoration: none;
  cursor: pointer;
  transition:
    background-color var(--motion-fast) var(--ease-standard),
    border-color var(--motion-fast) var(--ease-standard),
    color var(--motion-fast) var(--ease-standard);
}

.imloka-header__search-trigger:hover,
.imloka-header__menu-trigger:hover,
.imloka-header__lang-trigger:hover { background: var(--surface-alt); }

/* Open state carried by aria-expanded, same as every other disclosure control
   in the shell. The border is the only cue: a shape change, present or
   absent, so it still reads without colour (C11), deliberately without a
   background tint or glow, which read as too loud for a control that sits in
   the header on every page. */
.imloka-header__search-trigger[aria-expanded="true"],
.imloka-header__menu-trigger[aria-expanded="true"] {
  border-color: var(--primary);
  color: var(--primary);
}

/* Responsive header matrix (§Responsive Header Matrix): navigation clarity
 * outranks the other actions at narrow widths. */
.imloka-header__nav { display: none; }

.imloka-header__search-label,
.imloka-header__menu-label,
.imloka-header__lang-label { display: none; }

@media (min-width: 1024px) {
  .imloka-header__nav { display: block; }
  .imloka-header__search-label,
  .imloka-header__menu-label,
  .imloka-header__lang-label { display: inline; }
}

/* ------------------------------------------------------------------ footer */
/* Quiet, durable, institutional: not a second mega-menu. Uses --surface-alt
 * rather than a saturated Primary, and never depends on Account, Trending,
 * recommendation, or ad APIs to render. */

/* Every page shell already ends with its own trailing space: the mosaic's
   hard tile edge, a Load More control (archive-list.html/
   topic-archive-list.html: padding-block --space-10), .imloka-doc's
   padding-block (about/contact/newsletter/etc: --space-16), .imloka-article
   (--space-16), the account layout (--space-16), search's shell
   padding-bottom (--space-16). A large margin here on top of any of those is
   always additive, never a deliberate design choice; so the footer owes a
   single small, consistent gap (matching the header-to-content gap on every
   page) rather than a bigger default with page types opting out one by one. */
.imloka-footer {
  margin-top: var(--space-8);
  background: var(--surface-alt);
  border-top: 1px solid var(--border);
}

.imloka-footer__inner {
  display: grid;
  gap: var(--space-12);
  padding-block: var(--space-16) var(--space-12);
}

@media (min-width: 1024px) {
  .imloka-footer__inner {
    grid-template-columns: minmax(0, 22rem) 1fr;
    gap: var(--space-16);
    padding-block: var(--space-20) var(--space-12);
  }
}

/* Brand lockup (Public Brand Profile §3). The tagline sits tight under the
   wordmark so the two read as one mark, then a wider gap before the social row
   keeps the tagline from turning into a heading over the icons. The doc's
   "do not add explanatory copy after it" is the reason the footer statement
   was removed from this block rather than set below the tagline. */
.imloka-footer__brand .imloka-wordmark {
  height: 26px;
  margin-bottom: var(--space-2);
}

.imloka-footer__tagline {
  font-size: var(--type-body-l);
  line-height: var(--type-body-l-lh);
  font-weight: var(--weight-semibold);
  letter-spacing: var(--track-heading);
  color: var(--text);
}

/* Only rendered when site.footer.showStatement is true (see footer.html);
   kept here so flipping that switch needs no CSS change. */
.imloka-footer__statement {
  /* Sized to the brand column (~43ch at the 22rem desktop track), not narrower.
     The old 34ch predated the tagline and left the paragraph 73px short of the
     column, which forced a ragged fifth line. */
  max-width: 42ch;
  margin-top: var(--space-4);
}

.imloka-footer__social {
  display: flex;
  gap: var(--space-2);
  margin-top: var(--space-6);
}

.imloka-footer__social-link {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 36px;
  height: 36px;
  color: var(--text-muted);
  border: 1px solid var(--border);
  border-radius: var(--radius-pill);
  transition: color var(--motion-fast) var(--ease-standard),
              border-color var(--motion-fast) var(--ease-standard);
}

.imloka-footer__social-link:hover {
  color: var(--primary);
  border-color: var(--primary);
}

/* Mobile groups are expanded by default, never accordionised. */
.imloka-footer__nav {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: var(--space-8) var(--space-6);
}

@media (min-width: 768px) {
  .imloka-footer__nav { grid-template-columns: repeat(4, minmax(0, 1fr)); }
}

.imloka-footer__group-title {
  color: var(--text-muted);
  margin-bottom: var(--space-4);
}

.imloka-footer__list { display: grid; gap: var(--space-3); }

.imloka-footer__link {
  /* Footer links are navigation, not prose, so they owe the minimum target
     size rather than taking the inline-text exemption. Inline-flex with a
     min-height gives a real 36px+ target without visually spacing the list out. */
  display: inline-flex;
  align-items: center;
  min-height: 36px;
  padding-block: var(--space-1);
  color: var(--text);
  font-size: var(--type-ui);
  text-decoration: none;
  transition: color var(--motion-fast) var(--ease-standard);
}

.imloka-footer__link:hover {
  color: var(--primary);
  text-decoration: underline;
  text-underline-offset: 0.18em;
}

/* The one footer item that reopens the cookie banner instead of navigating:
 * same visual treatment as every other footer link, on a real <button> so it
 * doesn't need a fake href users could tab to and "follow" into nothing. */
.imloka-footer__link--button {
  background: none;
  border: 0;
  font: inherit;
  cursor: pointer;
}

.imloka-footer__strip { border-top: 1px solid var(--border); }

.imloka-footer__strip-inner {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-4) var(--space-6);
  padding-block: var(--space-6) var(--space-8);
}

.imloka-footer__institutional {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2) var(--space-6);
}

.imloka-footer__institutional .imloka-footer__link { font-size: var(--type-meta); }

/* On coarse pointers the target grows to the full 44px the spec asks for. */
@media (pointer: coarse) {
  .imloka-footer__link { min-height: var(--target-min); }
}

/* ------------------------------------------------------------- back to top */
/* A persistent, quiet return to the top of the page. Not in v2.1: an additive
 * convenience control, built from the existing icon-button primitive so it
 * inherits the target size, focus ring, hover and forced-colors treatment
 * rather than restating them. Recorded in the plan's §14a.
 *
 * Deliberately restrained: no label, no shadow stack, no accent fill. It is a
 * utility, and a loud utility in the corner of every page is exactly the kind
 * of ambient noise the design system exists to remove. */

.imloka-to-top {
  position: fixed;
  /* Logical properties: the corner follows the writing direction rather than
     being pinned to the right, so an RTL edition needs no override. */
  inset-block-end: var(--space-6);
  inset-inline-end: var(--space-6);
  z-index: var(--z-raised);
  background: var(--surface);
  border: 1px solid var(--border);
  color: var(--text-muted);
  transition:
    color var(--motion-fast) var(--ease-standard),
    border-color var(--motion-fast) var(--ease-standard),
    opacity var(--motion-base) var(--ease-standard),
    visibility var(--motion-base) var(--ease-standard);
}

.imloka-to-top:hover {
  background: var(--surface);
  border-color: var(--primary);
  color: var(--primary);
}

/* Without JavaScript the control is permanently visible: nothing can reveal it
   on scroll, and a link that never appears is worse than one that is always
   there. It still works: a native jump to #top. */
[data-js] .imloka-to-top {
  opacity: 0;
  visibility: hidden;
}

/* The reveal is conditional on no layer being open, rather than being fought
   afterwards by a more specific override. Expressing the condition once, in the
   selector, avoids a specificity race between "show when scrolled" and "hide
   behind a layer" - which the first version of this lost. */
[data-js]:not([data-layer]) .imloka-to-top[data-visible] {
  opacity: 1;
  visibility: visible;
}

/* Clear of the reading column on small screens, where the gutter is tight and
   a floating control can otherwise sit on top of body text. */
@media (max-width: 599px) {
  .imloka-to-top {
    inset-block-end: var(--space-4);
    inset-inline-end: var(--space-4);
  }
}

/* The no-JS path: layers open via :target, and nothing stamps data-layer, so
   the control has to notice the open layer itself. (With JS the reveal rule
   above already declines to fire.) */
body:has(.imloka-menu-layer:target) .imloka-to-top,
body:has(.imloka-search-layer:target) .imloka-to-top {
  opacity: 0;
  visibility: hidden;
}

@media (forced-colors: active) {
  .imloka-to-top { border-color: ButtonBorder; }
}
}

/* --- styles/patterns/style-guide.css */
@layer patterns {
/* Patterns: the /style/ system reference pages.
 *
 * These pages render the SAME partials and tokens as the site, never a
 * re-implementation, so they cannot drift from what they document. */

.imloka-style-page { padding-block: var(--space-12) var(--space-16); }

.imloka-style-header { margin-bottom: var(--space-12); }
.imloka-style-header__eyebrow { color: var(--text-muted); }
.imloka-style-header__title { margin-block: var(--space-3) var(--space-4); }
.imloka-style-header__lede { max-width: 60ch; color: var(--text-muted); }

.imloka-style-nav {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
  margin-top: var(--space-8);
  padding-bottom: var(--space-8);
  border-bottom: 1px solid var(--border);
}

.imloka-style-nav__link {
  display: inline-flex;
  align-items: center;
  min-height: 36px;
  padding-inline: var(--space-3);
  border: 1px solid var(--border);
  border-radius: var(--radius-xs);
  background: var(--surface);
  color: var(--text);
  font-size: var(--type-meta);
  font-weight: var(--weight-medium);
  text-decoration: none;
  transition:
    border-color var(--motion-fast) var(--ease-standard),
    color var(--motion-fast) var(--ease-standard);
}

.imloka-style-nav__link:hover { border-color: var(--primary); color: var(--primary); }

.imloka-style-nav__link[aria-current="page"] {
  border-color: var(--primary);
  color: var(--text-on-primary);
  background: var(--primary);
}

.imloka-style-section { margin-block: var(--space-16); }
.imloka-style-section > h2 { margin-bottom: var(--space-2); }

.imloka-style-section__note {
  max-width: 68ch;
  margin-bottom: var(--space-8);
  color: var(--text-muted);
  font-size: var(--type-meta);
  line-height: 1.6;
}

/* Quiet framing for a specimen so the specimen reads, not the frame. */
.imloka-specimen {
  padding: var(--space-6);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  background: var(--surface);
}

.imloka-specimen + .imloka-specimen { margin-top: var(--space-3); }

.imloka-specimen__meta {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2) var(--space-4);
  margin-bottom: var(--space-3);
  color: var(--text-muted);
  font-size: var(--type-meta);
  font-variant-numeric: tabular-nums;
}

.imloka-specimen__code {
  font-family: ui-monospace, "SF Mono", Menlo, Consolas, monospace;
  font-size: 0.9em;
}

/* --- token tables --------------------------------------------------------- */

.imloka-token-grid {
  display: grid;
  gap: var(--space-1);
  grid-template-columns: minmax(0, 1fr);
}

.imloka-token-row {
  display: grid;
  grid-template-columns: minmax(9rem, 14rem) minmax(4rem, 6rem) minmax(0, 1fr);
  align-items: center;
  gap: var(--space-4);
  padding: var(--space-2) var(--space-3);
  border-radius: var(--radius-xs);
  background: var(--surface);
  font-size: var(--type-meta);
}

.imloka-token-row__name {
  font-family: ui-monospace, "SF Mono", Menlo, Consolas, monospace;
  color: var(--text);
}

.imloka-token-row__value {
  color: var(--text-muted);
  font-variant-numeric: tabular-nums;
}

.imloka-token-row__bar {
  height: 14px;
  border-radius: var(--radius-xs);
  background: var(--primary);
  /* The bar is sized by the token it illustrates, so at narrow widths a large
     spacing value would exceed its track. Capping keeps the specimen honest
     about ordering while never pushing the page sideways. */
  max-width: 100%;
}

/* Below the tablet range the three-column row cannot hold its minimums, so it
   stacks: name and value on one line, the bar full-width beneath. */
@media (max-width: 560px) {
  .imloka-token-row {
    grid-template-columns: minmax(0, 1fr) auto;
    gap: var(--space-1) var(--space-3);
  }
  .imloka-token-row__bar { grid-column: 1 / -1; }
}

/* --- colour swatches ------------------------------------------------------ */

.imloka-theme-board {
  display: grid;
  gap: var(--space-6);
  grid-template-columns: repeat(auto-fit, minmax(15rem, 1fr));
}

/* Each board paints itself with its own theme tokens by carrying the same
   data attributes the root element uses: no duplicated colour values. */
.imloka-theme-card {
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  background: var(--page-bg);
  color: var(--text);
  overflow: hidden;
}

.imloka-theme-card__head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--space-3);
  padding: var(--space-4);
  background: var(--surface);
  border-bottom: 1px solid var(--border);
}

.imloka-theme-card__body { padding: var(--space-4); }

.imloka-swatch-row {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  padding-block: var(--space-1);
  font-size: var(--type-meta);
}

.imloka-swatch {
  flex: 0 0 auto;
  width: 30px; height: 30px;
  border: 1px solid var(--border);
  border-radius: var(--radius-xs);
}

.imloka-swatch-row__name {
  font-family: ui-monospace, "SF Mono", Menlo, Consolas, monospace;
  flex: 1 1 auto;
  min-width: 0;
}

.imloka-swatch-row__hex {
  color: var(--text-muted);
  font-variant-numeric: tabular-nums;
  text-transform: uppercase;
}

/* --- contrast audit ------------------------------------------------------- */

.imloka-contrast-table {
  width: 100%;
  border-collapse: collapse;
  font-size: var(--type-meta);
  font-variant-numeric: tabular-nums;
}

.imloka-contrast-table th,
.imloka-contrast-table td {
  padding: var(--space-2) var(--space-3);
  text-align: left;
  border-bottom: 1px solid var(--border);
}

/* A cross-reference alone in a cell is a navigation link, not a word in a
   sentence, so it owes the minimum target like any other. */
.imloka-contrast-table td > a:only-child {
  display: inline-flex;
  align-items: center;
  min-height: 36px;
}

.imloka-contrast-table th {
  font-weight: var(--weight-semibold);
  color: var(--text-muted);
  font-size: var(--type-label);
  text-transform: uppercase;
  letter-spacing: var(--track-label);
}

/* Numeric columns align right; a table of prose does not. Opt in per table
   rather than assuming every non-first column is a number. */
.imloka-contrast-table--numeric td:not(:first-child) { text-align: right; }

/* Pass/fail carries a glyph as well as colour: never colour alone (C11). */
.imloka-verdict { font-weight: var(--weight-semibold); }
.imloka-verdict--pass { color: var(--success); }
.imloka-verdict--fail { color: var(--danger); }

.imloka-scroll-x {
  overflow-x: auto;
  -webkit-overflow-scrolling: touch;
}

/* --- grid overlay --------------------------------------------------------- */

.imloka-grid-demo {
  display: grid;
  grid-template-columns: repeat(var(--grid-columns), minmax(0, 1fr));
  gap: var(--grid-gap);
}

.imloka-grid-demo__col {
  height: 72px;
  border-radius: var(--radius-xs);
  background: var(--secondary);
  border: 1px solid var(--border);
  display: grid;
  place-items: center;
  font-size: var(--type-label);
  color: var(--text-muted);
  font-variant-numeric: tabular-nums;
}

.imloka-measure-bar {
  height: 10px;
  border-radius: var(--radius-xs);
  background: var(--primary);
  max-width: 100%;
}

/* --- motion specimen ------------------------------------------------------ */

.imloka-motion-chip {
  display: grid;
  gap: 2px;
  place-content: center;
  min-width: 108px;
  padding: var(--space-4);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  background: var(--surface);
  text-align: center;
  font-size: var(--type-meta);
  cursor: default;
  /* --d is set per-chip from a --motion-* token; the property list is explicit
     because `transition: all` is prohibited. */
  transition:
    background-color var(--d) var(--ease-standard),
    color var(--d) var(--ease-standard),
    transform var(--d) var(--ease-standard);
}

.imloka-motion-chip small { color: var(--text-muted); font-variant-numeric: tabular-nums; }

.imloka-motion-chip:hover {
  background: var(--primary);
  color: var(--text-on-primary);
  transform: translateY(-2px);
}

.imloka-motion-chip:hover small { color: inherit; }

@media (prefers-reduced-motion: reduce) {
  /* The state change survives; only the spatial travel is dropped. */
  .imloka-motion-chip:hover { transform: none; }
}

/* --- state gallery -------------------------------------------------------- */

.imloka-state-row {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-4);
}

.imloka-state-row > div {
  display: grid;
  gap: var(--space-2);
  justify-items: center;
}

.imloka-state-row .imloka-meta { font-variant-numeric: tabular-nums; }

/* A forced state is outlined so a screenshot makes clear which one is under
   test; otherwise a regression image is ambiguous about its own subject. */
[data-forced="true"] {
  outline: 2px dashed var(--accent);
  outline-offset: 4px;
}
}
