/* ============================================================================
   Base — reset, primitives, and the ambient layers (WebGL canvas, boot).
   ========================================================================== */

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

html {
  -webkit-text-size-adjust: 100%;
  text-size-adjust: 100%;
  scrollbar-color: var(--ink-6) transparent;
  scrollbar-width: thin;
}

body {
  margin: 0;
  min-height: 100dvh;
  background: var(--void);
  color: var(--text-0);
  font-family: var(--font-ui);
  font-size: var(--t-15);
  line-height: var(--lh-15);
  /* NOTE: Inter's ss03 ("round quotes & commas") makes a thousands separator
     render as a dot at UI sizes — "3,077" reads as "3.077". Never enable it. */
  font-feature-settings: 'cv05' 1, 'cv08' 1, 'calt' 1, 'liga' 1;
  font-variation-settings: 'opsz' 16;
  letter-spacing: var(--tracking-tight);
  /* Deliberately NOT -webkit-font-smoothing: antialiased. On a dark surface it
     thins every stroke, and the first casualty is the tail of a comma: three
     independent reviewers read "3,077" as "3.077" at 13px. Legibility beats the
     slightly softer look. */
  -moz-osx-font-smoothing: auto;
  text-rendering: optimizeLegibility;
  overflow-x: hidden;
  overscroll-behavior-y: none;
}

h1, h2, h3, h4, h5, h6, p, figure, blockquote, dl, dd { margin: 0; }
ul, ol { margin: 0; padding: 0; list-style: none; }
img, svg, video, canvas { display: block; max-width: 100%; }
img { border-style: none; }

button, input, select, textarea {
  font: inherit;
  letter-spacing: inherit;
  color: inherit;
  background: none;
  border: 0;
  margin: 0;
  /* Chrome's UA button padding is 1px 6px. Left in, it insets every image
     inside a <button> tile by 6px a side and 1px top and bottom, which shows
     up as a coloured band down the edge of a media grid. */
  padding: 0;
}
button { cursor: pointer; -webkit-tap-highlight-color: transparent; }
button:disabled { cursor: not-allowed; }
textarea { resize: none; }

a { color: inherit; text-decoration: none; }

::selection { background: var(--accent); color: var(--accent-ink); }

/* --- Focus: visible, on-brand, and never on mouse clicks ------------------ */
:focus { outline: none; }
:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
  border-radius: var(--r-xs);
}

/* --- Scrollbars ----------------------------------------------------------- */
::-webkit-scrollbar { width: 10px; height: 10px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb {
  background: var(--ink-4);
  border: 3px solid transparent;
  background-clip: padding-box;
  border-radius: 99px;
}
::-webkit-scrollbar-thumb:hover { background: var(--ink-7); background-clip: padding-box; }

/* --- Skip link ------------------------------------------------------------ */
.skip-link {
  position: fixed; top: -100px; left: var(--s-6); z-index: var(--z-toast);
  padding: var(--s-4) var(--s-6);
  background: var(--accent); color: var(--accent-ink);
  border-radius: var(--r-sm); font-weight: 650;
  transition: top var(--d-2) var(--ease-out);
}
.skip-link:focus-visible { top: var(--s-6); }

.sr-only {
  position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
  max-width: 1px; max-height: 1px;
  overflow: hidden; clip: rect(0 0 0 0); clip-path: inset(50%); white-space: nowrap;
}
/* `width: 1px` is a *suggestion* to a table — auto table layout still takes the
   min-content width of the columns, so a visually-hidden data table happily
   sits 758px wide and hands the document a horizontal scrollbar. Never put
   .sr-only on a table directly; if it happens anyway, this contains it. */
table.sr-only { table-layout: fixed; contain: strict; }

/* ============================================================================
   THE BRAND MARK, AS INK RATHER THAN AS A PICTURE

   Both brand files are monochrome — the lockup is a set of paths under a single
   `fill="#ffffff"` on its root, the rooster one `fill:white`. Loaded through
   `<img>` that white is unreachable: an image is
   an image, no stylesheet reaches inside it, and `currentColor` resolves
   against the SVG's own root rather than the page. So the wordmark was white on
   every theme, including the eighteen whose background is also white, and the
   product's own name disappeared the moment a member chose Paper.

   The fix is to stop drawing the file and start using it as a stencil: the same
   URL as a `mask-image`, and `currentColor` as the paint. The mark is then
   text, and takes whatever ink the theme puts on the surface it sits on — all
   thirty-six of them, with no second asset to keep in sync and no `invert()`
   filter that would have to be undone the day the mark gains a colour.

   Being monochrome is what makes this safe, and it is a real constraint on the
   artwork: the day someone gives the rooster a red comb, this stops being
   correct and the file needs a real two-tone treatment.

   The rebrand to OGalo did not disturb any of this. The mark was already a
   gamefowl cock in full display — it was drawn for the old name and it fits the
   new one better than it fit the old — so only the word beside it was redrawn.
   ========================================================================== */
.brandink {
  display: block;
  background-color: currentColor;
  /* Prefixed first, unprefixed last: Safari still needs -webkit-mask-image. */
  -webkit-mask-repeat: no-repeat;  mask-repeat: no-repeat;
  -webkit-mask-position: center;   mask-position: center;
  -webkit-mask-size: 100% 100%;    mask-size: 100% 100%;
}
/* ----------------------------------------------------------------------------
   THE SAME STENCIL, NOW WITH AN OWNER-SETTABLE ARTWORK

   Every value below is `var(--brand-…, <what this file has always said>)`. The
   fallback is not a nicety: /brand.css is generated by server/brand.mjs and, on
   an install where nobody has opened the console's Brand screen, it declares
   almost nothing. So the defaults live here, in the stylesheet that is checked
   into the repo, and the generated sheet only speaks when it has something to
   say. Deleting the brand rows from the database restores this file's design
   rather than producing a page with no mark on it.

   `--brand-*-color` defaults to `currentColor`, which is what keeps the mark on
   the text ramp across all thirty-six themes. An operator who sets a colour is
   opting out of that, deliberately, and gets told so on the screen.
   ---------------------------------------------------------------------------- */

/* The aspect ratios are the files' own viewBoxes. An <img> got these for free
   from the intrinsic size; a masked box has no intrinsic anything, so the
   height has to come from somewhere and it may as well come from the artwork
   rather than from a number typed at each call site. */
.brandink--lockup {
  /* The lockup's viewBox, clear space included — see the note at the top of
     ogalo-lockup.svg. Move one and this has to move with it. */
  aspect-ratio: var(--brand-logo-ratio, 674 / 228);
  background-color: var(--brand-logo-color, currentColor);
  -webkit-mask-image: var(--brand-logo-src, url('/assets/brand/ogalo-lockup.svg'));
  mask-image: var(--brand-logo-src, url('/assets/brand/ogalo-lockup.svg'));
}
.brandink--mark {
  aspect-ratio: var(--brand-icon-ratio, 1);
  background-color: var(--brand-icon-color, currentColor);
  -webkit-mask-image: var(--brand-icon-src, url('/assets/brand/ogalo-icon-white.svg'));
  mask-image: var(--brand-icon-src, url('/assets/brand/ogalo-icon-white.svg'));
}
.brandink--mobile {
  aspect-ratio: var(--brand-m-ratio, 674 / 228);
  background-color: var(--brand-m-color, currentColor);
  -webkit-mask-image: var(--brand-m-src, url('/assets/brand/ogalo-lockup.svg'));
  mask-image: var(--brand-m-src, url('/assets/brand/ogalo-lockup.svg'));
}

/* ----------------------------------------------------------------------------
   THE WORDMARK AS TYPE

   A brand slot can be a word instead of a file, and the word has to be able to
   paint in the same frame as the rest of the shell — which rules out reading it
   from /api/bootstrap, because bootstrap answers after mount and the mark is on
   screen before that. So the text is a CSS `content:` value carried by a custom
   property, and BOTH nodes are always in the DOM: the generated sheet flips
   which one is displayed. No JavaScript participates, so there is no frame in
   which the wrong one is visible.

   `content: var(--brand-logo-text)` needs the property to hold a CSS *string*,
   quotes included — server/brand.mjs quotes and escapes it, and strips the five
   characters that could end the declaration early. */
.brandword {
  display: none;
  line-height: 1;
  white-space: nowrap;
  color: var(--brand-logo-color, currentColor);
  font-family: var(--brand-logo-font, var(--font-dis));
  font-size: var(--brand-logo-fs, 30px);
  font-weight: var(--brand-logo-fw, 700);
  font-style: var(--brand-logo-fi, normal);
  letter-spacing: var(--brand-logo-ls, -0.02em);
}
.brandword::after { content: var(--brand-logo-text, "OGalo"); }
.brandword--mobile {
  color: var(--brand-m-color, currentColor);
  font-family: var(--brand-m-font, var(--font-dis));
  font-size: var(--brand-m-fs, 22px);
  font-weight: var(--brand-m-fw, 700);
  font-style: var(--brand-m-fi, normal);
  letter-spacing: var(--brand-m-ls, -0.02em);
}
.brandword--mobile::after { content: var(--brand-m-text, "OGalo"); }

/* ============================================================================
   Ambient layers
   ========================================================================== */

#gl {
  position: fixed; inset: 0;
  width: 100%; height: 100%;
  z-index: var(--z-gl);
  pointer-events: none;
  /* The scene is the room the app sits in — it should never fight the content. */
  opacity: 0;
  transition: opacity 1600ms var(--ease-out);
}
#gl.is-ready { opacity: .62; }

.app-root {
  position: relative;
  z-index: var(--z-content);
  min-height: 100dvh;
}

.portal-root { position: relative; z-index: var(--z-modal); }

.toast-root {
  position: fixed; z-index: var(--z-toast);
  left: 50%; bottom: var(--s-9);
  transform: translateX(-50%);
  display: flex; flex-direction: column; gap: var(--s-4);
  align-items: center;
  pointer-events: none;
}

/* ============================================================================
   Boot splash
   ========================================================================== */
.boot {
  position: fixed; inset: 0; z-index: var(--z-toast);
  display: grid; place-content: center; gap: var(--s-9);
  justify-items: center;
  background: var(--bg-0);
  transition: opacity var(--d-5) var(--ease-out), visibility var(--d-5);
}
.boot.is-gone { opacity: 0; visibility: hidden; pointer-events: none; }
/* No drop-shadow: a critique round removed the accent glow this used to carry,
   on the grounds that no other element on the product repeats it. That ruling
   lived as a `filter: none` override in app.css and so only reached index.html;
   folded in here it reaches every entry point, and the override is gone. */
.boot__lockup {
  width: min(300px, 62vw);
  animation: bootPulse 2.4s var(--ease-in-out) infinite;
}
.boot__bar {
  width: 120px; height: 2px; border-radius: 2px;
  background: var(--ink-4); overflow: hidden;
}
.boot__bar i {
  display: block; height: 100%; width: 40%;
  background: var(--accent);
  border-radius: inherit;
  animation: bootSlide 1.1s var(--ease-in-out) infinite;
}
@keyframes bootPulse {
  0%, 100% { opacity: .55; transform: scale(.98); }
  50%      { opacity: 1;   transform: scale(1.02); }
}
@keyframes bootSlide {
  0%   { transform: translateX(-120%); }
  100% { transform: translateX(320%); }
}

/* ============================================================================
   Shared primitives
   ========================================================================== */

/* Frosted panel — used by headers, menus, sheets. The saturate() is what makes
   backdrop blur look like real glass instead of grey mud. */
.glass {
  background: var(--glass);
  backdrop-filter: blur(18px) saturate(180%);
  -webkit-backdrop-filter: blur(18px) saturate(180%);
}
@supports not (backdrop-filter: blur(1px)) {
  .glass { background: var(--glass-strong); }
}

/* A raised surface with a physical top bevel. */
.surface {
  background: var(--bg-2);
  border: 1px solid var(--line);
  border-radius: var(--r-lg);
  box-shadow: var(--e-2), var(--bevel);
}

.hairline { border-bottom: 1px solid var(--line); }

/* Text utilities that carry the type system rather than ad-hoc sizes. */
.t-display { font-family: var(--font-dis); font-weight: 800; letter-spacing: var(--tracking-display); }
.t-brand   { font-family: var(--font-brand); font-weight: 400; letter-spacing: 0; }
.t-mono    { font-family: var(--font-mono); font-variant-numeric: tabular-nums; }
.t-num     { font-variant-numeric: tabular-nums; font-feature-settings: 'tnum' 1; }
.t-dim     { color: var(--text-1); }
.t-meta    { color: var(--text-2); font-size: var(--t-13); line-height: var(--lh-13); }

.truncate { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; min-width: 0; }
.clamp-2 { display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; }
.clamp-3 { display: -webkit-box; -webkit-line-clamp: 3; -webkit-box-orient: vertical; overflow: hidden; }

/* Content-visibility on long lists keeps the feed cheap past a few hundred posts. */
.cv-auto { content-visibility: auto; contain-intrinsic-width: none; contain-intrinsic-height: auto 220px; }
