/* ============================================================================
   content.css — styles for everything the backend renders.

   Kept OUT of base.css deliberately: base.css is the hand-built OS shell you're
   about to redesign, and this file is the layer that dresses database content.
   Keeping them apart means a restyle can replace one without disturbing the
   other. Everything here uses the same custom properties base.css defines, so
   both display modes (1-bit mono / 8-bit arcade) are inherited for free.
   ========================================================================= */

/* ---------------------------------------------------------------- uploaded art
   The static build only ever put a mask-icon <span> inside these boxes. Real
   uploaded photos need to fill the frame instead of sitting in the icon's
   padding, so a box containing an <img> drops its padding and crops to fit. */

.thumb-box:has(img),
.gallery-image:has(img){ padding:0; }

.thumb-box img,
.gallery-image img{
  width:100%;
  height:100%;
  object-fit:cover;
  display:block;
}

/* Fallback for browsers without :has() — the image still fills, the box just
   keeps its padding. Harmless, and Safari/Chrome/Firefox have shipped :has()
   since 2023 so this is only ever hit by something very old. */
.tile-icon-photo img{
  width:100%;
  height:100%;
  object-fit:contain;
  display:block;
}

/* ------------------------------------------------------------- content blocks
   Every block the editor produces is a .cb. They stack in a .case-body. */

.case-body{ display:block; }

.cb{ margin:0 0 1.35rem 0; }
.cb:last-child{ margin-bottom:0; }

/* A heading block opens a new beat, so it gets air above it and a rule —
   except when it's the first thing on the page. */
.cb-heading{
  margin-top:2.5rem;
  padding-top:2.5rem;
  border-top:1px dashed var(--muted);
}
.case-body > .cb-heading:first-child{
  margin-top:0;
  padding-top:0;
  border-top:none;
}

.cb-kicker{
  font-family:var(--font-mono);
  font-size:.62rem;
  letter-spacing:.1em;
  text-transform:uppercase;
  color:var(--muted);
  margin:0 0 .6rem 0;
}

.cb-title{
  font-family:var(--font-pixel);
  font-size:clamp(.85rem,2.4vw,1.05rem);
  line-height:1.5;
  margin:0;
}

.cb-text p{
  font-size:clamp(.95rem,2.2vw,1.05rem);
  line-height:1.7;
  max-width:62ch;
  margin:0 0 1rem 0;
}
.cb-text p:last-child{ margin-bottom:0; }

.cb-text a{ color:inherit; text-decoration:underline; text-underline-offset:.2em; }
body.arcade-mode .cb-text a{ color:var(--accent-b); }

/* ------------------------------------------------------------------- images */

.cb-image{ margin:0 0 1.5rem 0; }

.cb-image img{
  display:block;
  width:100%;
  height:auto;
  border:1px solid var(--muted);
}

.cb-image-inline img{ max-width:min(420px,100%); }
.cb-image-wide img{ border-left:none; border-right:none; }

.cb-caption{
  font-family:var(--font-mono);
  font-size:.62rem;
  letter-spacing:.05em;
  color:var(--muted);
  margin:.55rem 0 0 0;
  max-width:62ch;
}

/* An image row. minmax(min(...)) + min-width:0 are the same grid-blowout guards
   base.css uses on .gallery-card: without them a long caption or a wide image's
   intrinsic min-content width pushes the tracks past the container on a phone. */
.cb-row{
  display:grid;
  grid-template-columns:repeat(auto-fit, minmax(min(180px,100%), 1fr));
  gap:.75rem;
  margin:0 0 1.5rem 0;
}

.cb-row-img{
  width:100%;
  height:100%;
  min-width:0;
  object-fit:cover;
  aspect-ratio:1/1;
  border:1px solid var(--muted);
  display:block;
}

/* ------------------------------------------------------------------- quotes */

.cb-quote{
  margin:1.75rem 0;
  padding:0 0 0 1.1rem;
  border-left:2px solid var(--card-border);
}

.cb-quote p{
  font-family:var(--font-mono);
  font-size:clamp(1rem,2.6vw,1.2rem);
  line-height:1.6;
  margin:0;
  max-width:56ch;
}

.cb-quote cite{
  display:block;
  margin-top:.65rem;
  font-family:var(--font-mono);
  font-size:.62rem;
  letter-spacing:.08em;
  text-transform:uppercase;
  font-style:normal;
  color:var(--muted);
}

body.arcade-mode .cb-quote{ border-left-color:var(--accent-a); }

/* ------------------------------------------------------------------ spacers */

.cb-spacer-sm{ height:1rem; }
.cb-spacer-md{ height:2.5rem; }
.cb-spacer-lg{ height:5rem; }

/* --------------------------------------------------------------- downloads */

.btn-download-size{
  font-size:.55rem;
  opacity:.7;
  margin-left:.4rem;
  letter-spacing:.04em;
}

/* ------------------------------------------------- multi-paragraph post body
   The template now emits <div class="post-body"> wrapping real <p>s, so the
   type styles have to move onto the children. */

.post-body p{
  font-size:clamp(.95rem,2.2vw,1.02rem);
  line-height:1.7;
  max-width:62ch;
  margin:0 0 1rem 0;
}
.post-body p:last-child{ margin-bottom:0; }
.post-body a{ color:inherit; text-decoration:underline; text-underline-offset:.2em; }

/* -------------------------------------------------------- gallery entry page */

.entry-tag{
  display:inline-block;
  font-family:var(--font-mono);
  font-size:.6rem;
  letter-spacing:.1em;
  text-transform:uppercase;
  color:var(--muted);
  border:1px solid var(--muted);
  padding:.25rem .5rem;
  margin:0 0 1rem 0;
}

/* Opt-in per entry — see gallery_entry_show_date(). No border, unlike the
   tag: it's a byline, not a label something belongs to. */
.entry-date{
  font-family:var(--font-mono);
  font-size:.65rem;
  letter-spacing:.06em;
  color:var(--muted);
  margin:0 0 1rem 0;
}

.entry-cover{ margin-bottom:2rem; }

.entry-nav{
  display:flex;
  align-items:center;
  justify-content:space-between;
  gap:.75rem;
  flex-wrap:wrap;
  margin:0 0 1.5rem 0;
  font-family:var(--font-mono);
  font-size:.65rem;
  letter-spacing:.04em;
}

.entry-nav-link{
  color:var(--muted);
  text-decoration:none;
  border:1px solid transparent;
  padding:.4rem .5rem;
  min-width:0;
  overflow-wrap:anywhere;
  transition:color .18s ease, border-color .18s ease;
}

.entry-nav-hub{ text-transform:uppercase; }

.entry-nav-link:focus-visible{ color:var(--fg); border-color:var(--card-border); }
@media (hover:hover) and (pointer:fine){
  .entry-nav-link:hover{ color:var(--fg); border-color:var(--card-border); }
}

/* ------------------------------------------------------------- about page */

.about-portrait{
  margin:0 0 1.5rem 0;
  max-width:280px;
}

.about-portrait img{
  display:block;
  width:100%;
  height:auto;
  border:1px solid var(--muted);
}

/* ------------------------------------------------ window aspect-ratio control
   Lives in the right-hand cluster with the display switch, NOT centred in the
   bar. It was centred, with the breadcrumb capped at half the bar's width to
   keep clear of it — but the breadcrumb doesn't start at the bar's left edge,
   it starts after the ◀▶ buttons, so that cap always left its right edge about
   50px past centre. It collided at every window width.
   In flow after a flex:1 breadcrumb, a collision is impossible by
   construction: the breadcrumb gets the space that's left and truncates. */

.ratio-btn{
  flex-shrink:0;
  display:inline-flex;
  align-items:center;
  gap:.35rem;
  padding:.15rem .45rem;
  border:1px solid currentColor;
  background:none;
  color:inherit;
  font-family:var(--font-mono);
  font-size:.58rem;
  letter-spacing:.08em;
  line-height:1;
  cursor:pointer;
  opacity:.65;
  white-space:nowrap;
  transition:opacity .18s ease, background-color .18s ease, color .18s ease;
}

.ratio-btn-icon{
  width:11px;
  height:11px;
  flex:0 0 auto;
}

.ratio-btn:focus-visible{ opacity:1; outline:1px solid currentColor; outline-offset:2px; }

/* Hover only brightens — the filled look is reserved for the ON state, so the
   two can't be confused. */
@media (hover:hover) and (pointer:fine){
  .ratio-btn:hover{ opacity:1; }
}

/* ON — fitted. The fill is the bar's own foreground colour, so it lands on
   each display mode's natural "lit" colour: green on the dark 8-bit bar, and
   an inverted black-on-white chip in 1-bit, where a white fill would simply
   disappear into the white bar. */
.ratio-btn.is-active{
  opacity:1;
  background:var(--header-fg);
  border-color:var(--header-fg);
  color:var(--header-bg);
}

/* The glow is arcade-only; a bloom on a crisp 1-bit interface looks like a
   rendering bug rather than an indicator. */
body.arcade-mode .ratio-btn.is-active{
  box-shadow:0 0 6px color-mix(in srgb, var(--header-fg) 55%, transparent);
}

@media (hover:hover) and (pointer:fine){
  .ratio-btn.is-active:hover{ opacity:.88; }
}

/* Desktop-only affordance: below the breakpoint the window is a full-screen
   app shell, so there is no shape to fit. */
@media (max-width:768px){
  .ratio-btn{ display:none !important; }
}

/* --------------------------------------------------------- display switch
   Sits immediately beside FIT. Deliberately NOT another bordered chip: two
   identical-looking controls side by side, one a momentary action and one a
   two-state mode, is a trap. A sliding knob says "two states" on sight. */

.theme-switch{
  display:inline-flex;
  align-items:center;
  gap:.4rem;
  flex-shrink:0;
  padding:.2rem .1rem;
  border:none;
  background:none;
  color:inherit;
  font-family:var(--font-mono);
  font-size:.58rem;
  letter-spacing:.08em;
  line-height:1;
  cursor:pointer;
  opacity:.75;
  white-space:nowrap;
  -webkit-tap-highlight-color:transparent;
  touch-action:manipulation;
  transition:opacity .18s ease;
}

.theme-switch-track{
  position:relative;
  display:block;
  width:26px;
  height:13px;
  flex:0 0 auto;
  border:1px solid currentColor;
  background:none;
}

.theme-switch-knob{
  position:absolute;
  top:1px;
  left:1px;
  width:9px;
  height:9px;
  background:currentColor;
  transition:transform .18s cubic-bezier(.22,.61,.36,1);
}

/* 8-bit = knob right. 1-bit = knob left. The label says which, so the switch
   only has to carry the sense of "there are two of these". */
.theme-switch[aria-checked="true"] .theme-switch-knob{ transform:translateX(13px); }

.theme-switch-label{ min-width:3.1em; text-align:left; }

.theme-switch:focus-visible{ opacity:1; outline:1px solid currentColor; outline-offset:2px; }

@media (hover:hover) and (pointer:fine){
  .theme-switch:hover{ opacity:1; }
}
@media (hover:none){
  .theme-switch:active{ opacity:1; }
}

/* Phone: keep the switch, drop the word — the bar is narrow. */
@media (max-width:768px){
  .theme-switch-label{ display:none; }
}

/* The old bottom-of-screen control is gone. Hide any leftover instance so a
   cached base.js can't leave a stray button floating over the page. */
.os-controls{ display:none !important; }

/* The window moving and resizing — the ONLY definition of this. base.css used
   to carry a second one; since this file loads later, that one never applied,
   and edits to it appeared to do nothing. If this needs changing, change it
   here.

   .48s rather than .34s: the window travels a long way on a shape change and at
   a third of a second the eye reads the endpoints rather than the movement,
   which is what "it just pops" describes. Position and size share one duration
   and one curve so a re-shape moves and grows as a single gesture. */
.card.is-animating{
  transition:width .48s cubic-bezier(.22,.61,.36,1),
             height .48s cubic-bezier(.22,.61,.36,1),
             left .48s cubic-bezier(.22,.61,.36,1),
             top .48s cubic-bezier(.22,.61,.36,1);
}

/* The boot-up grow is slower and overshoots very slightly, so opening the site
   reads as the machine coming to life rather than a window being dragged. */
.card.is-animating.is-booting{
  transition-duration:.72s;
  transition-timing-function:cubic-bezier(.16,1.02,.3,1);
}

@media (prefers-reduced-motion: reduce){
  .card.is-animating{ transition:none; }
}

/* ------------------------------------------------------------ resize handle
   base.css draws a faint 18px diagonal hatch in the corner — easy to miss, and
   it's the only affordance telling you the window resizes at all. Same single
   mark, just bigger, brighter, and reacting to the pointer. */

.resize-handle{
  width:26px;
  height:26px;
}

.resize-handle::before{
  inset:5px;
  opacity:.95;
  background-image:repeating-linear-gradient(135deg,
    var(--fg) 0 2px, transparent 2px 6px);
  transition:opacity .18s ease;
}

body.arcade-mode .resize-handle::before{
  background-image:repeating-linear-gradient(135deg,
    var(--card-border) 0 2px, transparent 2px 6px);
}

@media (hover:hover) and (pointer:fine){
  .resize-handle:hover::before{ opacity:1; }
}

/* ============================================================ PAGE MODE
   A page that has escaped the OS window: no chrome, no title bar, no resize
   handle, edge to edge in the browser. Everything a normal website page is.

   base.css assumes .card/.stage exist and constrains the body accordingly, so
   this block explicitly undoes those assumptions rather than relying on them
   not applying. */

body.page-mode{
  overflow-y:auto;
  overflow-x:hidden;
  min-height:100vh;
  min-height:100dvh;
  /* A column, so the footer sits at the bottom of a short page instead of
     floating halfway up it, and .page-body can be told to take the slack. */
  display:flex;
  flex-direction:column;
  padding:0;
  margin:0;
}

.page-body{
  display:block;
  flex:1 0 auto;
  width:100%;
  max-width:none;
  margin:0 auto;
  padding:clamp(2.5rem,6vw,4.5rem) clamp(1.1rem,5vw,3rem) 3rem;
}

/* ---------------------------------------------------------------------------
   A full web page whose content is one embedded document.

   This layout is exactly one screen: the bar on top, the document filling
   everything under it, and the footer floating over the bottom of it. Nothing
   scrolls in the outer page — the document does its own scrolling inside its
   frame, which is what a landing page expects.

   Everything the site would normally stack under the content (the rule, the
   prev/next entry links, the admin edit button) is hidden here. They were what
   made the bottom of the page a large empty band: a full-bleed design ends at
   the bottom of the screen, and site furniture below it reads as dead space.
   Navigation is not lost — ◀ ▶, the breadcrumb and the star are all in the bar,
   and the footer keeps the way back.
   --------------------------------------------------------------------------- */
body.page-mode:has(.cb-doc),
body.page-mode:has(.cb-app-full){
  height:100vh;
  height:100dvh;
  overflow:hidden;
}

body.page-mode .page-body:has(.cb-doc),
body.page-mode .page-body:has(.cb-app-full){
  padding:0;
  display:flex;
  flex-direction:column;
  min-height:0;   /* or the flex child refuses to shrink below its content */
}

body.page-mode .page-body:has(.cb-doc) > .case-body,
body.page-mode .page-body:has(.cb-app-full) > .case-body{
  flex:1;
  min-height:0;
  display:flex;
  flex-direction:column;
  max-width:none;
  margin:0;
}

/* The site's own trailing furniture, hidden on this one layout. */
body.page-mode .page-body:has(.cb-doc) > .divider,
body.page-mode .page-body:has(.cb-doc) > .entry-nav,
body.page-mode .page-body:has(.cb-doc) > .btn-download,
body.page-mode .page-body:has(.cb-app-full) > .divider,
body.page-mode .page-body:has(.cb-app-full) > .entry-nav,
body.page-mode .page-body:has(.cb-app-full) > .btn-download{
  display:none;
}

/* The footer floats over the bottom of the document rather than sitting under
   it, and fades in instead of starting at a hard edge — so it reads as part of
   whatever is behind it rather than as a band bolted underneath. The gradient
   is in the site's own background colour, which is what the OS shell is; a page
   with a light background will show it as a soft darkening, which is the
   intended "shadow" either way. */
body.page-mode:has(.cb-doc) .page-footer,
body.page-mode:has(.cb-app-full) .page-footer{
  position:fixed;
  left:0;
  right:0;
  bottom:0;
  z-index:29;
  margin:0;
  border-top:none;
  padding:3.5rem clamp(1.1rem,5vw,3rem) 1rem;
  gap:.4rem;
  background:linear-gradient(to top,
    var(--bg) 0%,
    var(--bg) 35%,
    rgba(0,0,0,.55) 70%,
    transparent 100%);
  /* The transparent upper half of the gradient sits over the document; clicks
     have to pass through it or it would swallow the top of whatever is under
     there. The footer's own children take their events back. */
  pointer-events:none;
}
body.page-mode:has(.cb-doc) .page-footer > *,
body.page-mode:has(.cb-app-full) .page-footer > *{ pointer-events:auto; }

body.page-mode:has(.cb-doc) .page-footer .news-line,
body.page-mode:has(.cb-app-full) .page-footer .news-line{ min-height:0; }

/* A full-bleed app on a chromeless page is the same layout as an embedded
   document: it IS the page. Drop the ratio, the border and the trailing
   margin, and let the frame take every pixel under the bar. Without this the
   app kept its aspect box inside a full-height column and left a dead band
   below it — the exact thing this layout was written to remove. */
body.page-mode .page-body:has(.cb-app-full) > .case-body > .cb-app-full{
  flex:1;
  min-height:0;
  margin:0;
  display:flex;
}
body.page-mode .page-body:has(.cb-app-full) .cb-app-full > .app-frame{
  aspect-ratio:auto;
  height:100%;
  flex:1;
  min-height:0;
  border:none;
}

/* Text stays readable while media can still run full-bleed. */
.page-body > *{ max-width:74ch; margin-left:auto; margin-right:auto; }

.page-body > .cb-image-wide,
.page-body > .cb-app-wide,
.page-body > .cb-app-full,
.page-body > .cb-row,
.page-body > .entry-cover{
  max-width:min(1400px,100%);
}

.page-body .case-body{ max-width:none; }
.page-body .case-body > *{ max-width:74ch; margin-left:auto; margin-right:auto; }
.page-body .case-body > .cb-image-wide,
.page-body .case-body > .cb-app-wide,
.page-body .case-body > .cb-app-full,
.page-body .case-body > .cb-row{ max-width:min(1400px,100%); }

/* The bar at the top of a full web page — what's left of the OS shell, so the
   site's navigation survives even when the window doesn't. Same controls in the
   same order as the window's title bar, so nothing has to be relearned when a
   page escapes the window.

   It's sticky rather than fixed: a fixed bar would sit on top of the page's own
   content and every page would need to know to leave room for it. Sticky takes
   its own space in the flow and then stays put while you scroll. */
.page-bar{
  position:sticky;
  top:0;
  z-index:30;
  display:flex;
  align-items:center;
  gap:.7rem;
  padding:.55rem clamp(.7rem,2vw,.9rem);
  background:var(--header-bg);
  color:var(--header-fg);
  border-bottom:2px solid var(--card-border);
  /* Same typography as .chrome, and it has to be stated rather than inherited:
     .crumb and .nav-btn both use font-family:inherit, so in a bar that didn't
     set one they fell back to the body face — a different font from the OS
     window's, and ◀ / ▶ rendered at two different widths in it. */
  font-family:var(--font-mono);
  font-size:.65rem;
  letter-spacing:.06em;
  text-transform:uppercase;
}

.page-bar .chrome-left{ display:flex; align-items:center; gap:.4rem; }
.page-bar .crumb{ flex:1; min-width:0; }
.page-bar .theme-switch{ flex:0 0 auto; }
.page-bar .chrome-home-icon{ flex:0 0 auto; margin-left:.2rem; }

.page-footer{
  border-top:1px dashed var(--muted);
  margin-top:3rem;
  padding:1.1rem clamp(1.1rem,5vw,3rem) calc(1.1rem + env(safe-area-inset-bottom, 0px));
  display:flex;
  flex-direction:column;
  gap:.5rem;
  align-items:center;
  text-align:center;
}

/* In the window the ticker is a pinned footer with its own negative margin;
   here it's just a line of text in the flow. */
.page-footer .news-line{
  margin:0;
  position:static;
  border:none;
  padding:0;
  max-width:74ch;
}

.page-back{
  font-family:var(--font-mono);
  font-size:.62rem;
  letter-spacing:.1em;
  text-transform:uppercase;
  color:var(--muted);
  text-decoration:none;
  border:1px solid var(--muted);
  padding:.45rem .7rem;
  transition:color .18s ease, border-color .18s ease;
}

@media (hover:hover) and (pointer:fine){
  .page-back:hover{ color:var(--fg); border-color:var(--card-border); }
}

/* -------------------------------------------------------------- app blocks
   An interactive piece embedded in a page. The frame is sandboxed in markup;
   here it just needs to hold its shape and not blow out the grid. */

.cb-app{ margin:0 0 1.5rem 0; }

.app-frame{
  position:relative;
  width:100%;
  /* Set from the inline --app-aspect / --app-height that app_render() emits.
     Read here rather than written inline so the full-page layout can override
     the shape with a plain rule instead of !important. */
  aspect-ratio:var(--app-aspect,16/9);
  height:var(--app-height,auto);
  border:1px solid var(--muted);
  background:var(--gray-1);
  overflow:hidden;
}

.app-frame iframe{
  position:absolute;
  inset:0;
  width:100%;
  height:100%;
  border:0;
  display:block;
  /* Every fresh iframe navigation starts on a plain white canvas until ITS
     OWN stylesheet is parsed and applied — normally masked by the browser
     holding the first paint of a new document, but that's a heuristic, not
     a guarantee, and Firefox is looser about it than Chrome. Against this
     site's all-black theme, a slow one shows as a stark white flash before
     the embedded page's own background:#000 takes over. This is the
     backstop: the iframe ELEMENT's own background paints instantly (it's
     part of the already-loaded parent page, no fetch to wait on), so
     there's never a white frame to see regardless of how the embedded
     document's own load goes. Only matters pre-paint — once the embedded
     page renders anything, its own background is what actually shows. */
  background:var(--bg);
}

.cb-app-inline .app-frame{ max-width:min(560px,100%); }
.cb-app-wide .app-frame{ border-left:none; border-right:none; }

/* Full bleed: out to the edges of the card, not just to the 1400px column.
   The negative margin cancels .page-body's horizontal padding, which is the
   only thing between the block and the window's edge. The dropdown promised
   this and previously delivered a bordered box the same width as Wide. */
.page-body > .cb-app-full,
.page-body .case-body > .cb-app-full{
  max-width:none;
  margin-left:calc(-1 * clamp(1.1rem,5vw,3rem));
  margin-right:calc(-1 * clamp(1.1rem,5vw,3rem));
}
.cb-app-full .app-frame{ border-left:none; border-right:none; }

body.arcade-mode .app-frame{ border-color:var(--card-border); }

/* ------------------------------------------------- home: the centrepiece
   The ASCII loading bar is the default, but it can be swapped for an image,
   a GIF, or a live web animation. These sizes apply to the swapped-in ones. */

.loader-media,
.loader-app{
  margin:0;
  width:100%;
}

/* Width, ceiling, height and alignment come from the admin panel as an inline
   style — see home_loader_style(). With no height pinned, the image's own
   proportions decide: no aspect-ratio needed, the box is just as tall as it
   needs to be. */
.loader-media img{
  display:block;
  width:100%;
  height:auto;
}

/* A pinned height (home_loader_fit_class() adds .has-height) never distorts
   the image — it keeps its own aspect ratio and object-fit decides what
   happens to the leftover space. Contain letterboxes; only the Full width
   preset (.is-full) switches to cover, which crops instead, because "full
   width" is asking the picture to fill the box edge to edge. */
.loader-media.has-height{ overflow:hidden; }
.loader-media.has-height img{
  height:100%;
  object-fit:contain;
}
.loader-media.has-height.is-full img{ object-fit:cover; }

.loader-app .app-frame{ border:none; background:transparent; }

/* Same override for a web animation: a pinned height wins over the app's own
   declared aspect ratio (--app-aspect / --app-height from app_render()).
   app_render() wraps .app-frame in a .cb-app-full div of its own, and a
   percentage height only resolves against a parent whose OWN height is
   definite — so without this the 100% below had nothing to resolve against
   and the frame collapsed to zero. */
.loader-app.has-height > .cb-app-full{
  height:100%;
  margin:0;
}
.loader-app.has-height .app-frame{
  height:100%;
  aspect-ratio:auto;
}

/* ------------------------------------------------- home: below-the-fold cards
   Featured sections fill the top grid; everything else lives down here. The
   breakpoint is a CONTAINER query, matching .select-grid — the window is
   user-resizable, so a viewport-based @media would stay in desktop layout
   even after the window itself is dragged narrow. */

.home-above{ margin:0 0 1.5rem 0; }
.home-below{ margin:1.75rem 0 0 0; }

.overflow-sections{ margin-top:1.75rem; }

.overflow-heading{ margin-bottom:1rem; }

.overflow-grid{
  display:grid;
  grid-template-columns:1fr;
  gap:.6rem;
}
.overflow-grid > *{ min-width:0; }

@container (min-width:481px){
  .overflow-grid{
    grid-template-columns:repeat(auto-fill, minmax(min(220px,100%), 1fr));
  }
}

/* Overflow cards are quieter than the featured three — same component, less
   emphasis, so the top row still reads as the main choice. Size stays as it
   was; what's added is a proper home for the icon. */
.tile-overflow{
  display:flex;
  /* Stated explicitly, all of it. These used to be inherited from .tile-select,
     which was a horizontal "tape" — then the featured cards became a two-column
     grid of centred cards and this quietly followed them, because a rule that
     depends on another component's defaults breaks the moment that component
     changes. An overflow card is its own thing: a row, text left, art right. */
  flex-direction:row;
  align-items:center;
  justify-content:flex-start;
  text-align:left;
  gap:.7rem;
  padding:.85rem;
}

.tile-overflow .tile-head{ justify-content:flex-start; }

.tile-overflow .tile-text{
  flex:1 1 auto;
  width:auto;
  min-width:0;   /* long labels truncate instead of blowing the grid track out */
}

/* One line, clipped — these are a list, not a set of cards with descriptions. */
.tile-overflow .tile-sub{
  white-space:nowrap;
  overflow:hidden;
  text-overflow:ellipsis;
}

.tile-overflow .tile-title{ font-size:.72rem; }

/* The icon slot: a reserved square, always present, so every card in the row
   is the same shape whether or not art has been chosen yet. */
.tile-overflow .tile-icon{
  order:1;             /* after the text — art on the right, like .tile-h */
  flex:0 0 auto;
  width:38px;
  height:38px;
  margin:0;
  display:flex;
  align-items:center;
  justify-content:center;
  border:1px solid var(--muted);
  background:var(--grid-line);
  padding:5px;
  transition:border-color .2s ease, color .2s ease;
}

.tile-overflow .tile-icon .pixel-icon,
.tile-overflow .tile-icon img{ width:100%; height:100%; }
.tile-overflow .tile-icon img{ object-fit:cover; }

/* Nothing chosen yet — a dashed placeholder rather than a hole. */
.tile-overflow .tile-icon-empty{ border-style:dashed; opacity:.5; }

.tile-overflow:focus-visible .tile-icon{ border-color:var(--card-border); }
@media (hover:hover) and (pointer:fine){
  .tile-overflow:hover .tile-icon{ border-color:var(--card-border); }
}

/* On desktop base.css turns every .tile-select into a centred column with the
   icon stacked on top — right for the featured three, wrong for these. The
   "More" cards stay a horizontal row: text left, icon right.
   Container query, not @media, matching base.css: the window is resizable, so
   this has to key off the card's own width, not the viewport's. */
@container (min-width:560px){
  .tile-overflow{
    flex-direction:row;
    align-items:center;
    justify-content:space-between;
    text-align:left;
    gap:.9rem;
    padding:.9rem 1rem;
  }

  .tile-overflow .tile-head{ justify-content:flex-start; }
  .tile-overflow .tile-text{ width:auto; }

  .tile-overflow .tile-icon{
    order:1;                /* after the text in source order = on the right */
    width:40px;
    height:40px;            /* overrides the featured cards' clamp() sizing */
  }
}

/* --------------------------------------------------- admin-only affordances
   The "+ New …" placeholders were inert in the static build. They're real
   links now (shown only when you're signed in), so the not-allowed cursor and
   the suppressed press state have to be undone. */

a.gallery-add,
a.post-add,
a.tile-add{
  cursor:pointer;
  text-decoration:none;
}

a.gallery-add:active,
a.post-add:active,
a.tile-add:active{
  border-color:var(--card-border);
  color:var(--fg);
}

@media (hover:hover) and (pointer:fine){
  a.gallery-add:hover,
  a.post-add:hover,
  a.tile-add:hover{
    border-color:var(--card-border);
    color:var(--fg);
  }
}

.post-edit-link{
  display:inline-block;
  font-family:var(--font-mono);
  font-size:.6rem;
  letter-spacing:.08em;
  text-transform:uppercase;
  color:var(--muted);
  text-decoration:none;
  border-bottom:1px dotted var(--muted);
  margin-top:.4rem;
}

/* A draft is visible only to you — mark it unmistakably so a draft is never
   mistaken for something the public can see. */
.draft-banner{
  font-family:var(--font-mono);
  font-size:.62rem;
  letter-spacing:.08em;
  text-transform:uppercase;
  border:1px dashed var(--star);
  color:var(--star);
  padding:.6rem .75rem;
  margin:0 0 1.25rem 0;
}

.is-draft{ opacity:.62; }
.is-draft .gallery-tag,
.is-draft .tile-tag,
.is-draft .post-date{ color:var(--star); }

/* ---------------------------------------------------------------------------
   Embedded document — a Raw HTML block that is a whole standalone page.

   It renders in an iframe rather than inline (see html-frame.php for why), so
   the page inside keeps its own theme, its own reset, its own background edge
   to edge, and can't reach out into the site around it. Nothing here styles the
   contents; there's nothing to style. All this does is decide how much room the
   frame gets.
   --------------------------------------------------------------------------- */
.cb-doc{
  margin:0;
  width:100%;
  /* Inside the OS window there is no "rest of the screen" to fill, so a tall
     frame with its own scroll is the honest answer. Full web page mode
     overrides this below and hands it the whole viewport. */
  height:min(78vh, 900px);
}

.cb-doc .doc-frame{
  display:block;
  width:100%;
  height:100%;
  border:1px solid var(--card-border);
  background:#fff;   /* until the document paints its own */
}

/* Full web page: the frame takes everything the bar and footer don't. */
body.page-mode .cb-doc{
  flex:1;
  height:auto;
  min-height:0;
}
body.page-mode .cb-doc .doc-frame{ border:none; }

/* The case-body wrapper constrains children to a reading width; an embedded
   document is not text and must not be. */
.case-body > .cb-doc,
.page-body > .cb-doc{ max-width:none; }

/* ---------------------------------------------------------------------------
   An embedded document inside the OS window.

   Same idea as the full-page case: the document fills the box. Everything
   between the title bar and the news ticker belongs to it — no padding, no
   reading width, and none of the site's trailing furniture (the rule, the
   prev/next links, the admin edit button), which is what was leaving a large
   empty band under a design that had already ended at the bottom of its frame.

   The card body normally scrolls; here it must not. The document scrolls itself
   inside the frame, and two nested scrollers on the same gesture is the thing
   that makes a page feel broken.
   --------------------------------------------------------------------------- */
.card-body:has(> .case-body > .cb-doc){
  padding:0;
  overflow:hidden;
  display:flex;
  flex-direction:column;
  min-height:0;
}

.card-body:has(> .case-body > .cb-doc) > .case-body{
  flex:1;
  min-height:0;
  display:flex;
  flex-direction:column;
  max-width:none;
  margin:0;
}

.card-body:has(> .case-body > .cb-doc) .cb-doc{
  flex:1;
  height:auto;
  min-height:0;
}

/* No border either — the card already has one, and a second line a pixel inside
   it just reads as a mistake. */
.card-body:has(> .case-body > .cb-doc) .doc-frame{ border:none; }

.card-body:has(> .case-body > .cb-doc) > .divider,
.card-body:has(> .case-body > .cb-doc) > .entry-nav,
.card-body:has(> .case-body > .cb-doc) > .btn-download{
  display:none;
}

/* ---------------------------------------------------------------------------
   A full-bleed app inside the OS window.

   The same layout, for the same reason. An embedded document and an app set to
   Full bleed are both "this piece IS the page" — everything between the title
   bar and the news ticker belongs to them. Without this the app kept its aspect
   box inside a padded, reading-width column and left the same empty band under
   it that the document rules were written to remove.

   Full bleed therefore means the app owns the card. An app placed INSIDE an
   article, with text around it, wants Wide — which stays in the normal flow and
   changes none of this.
   --------------------------------------------------------------------------- */
.card-body:has(> .case-body > .cb-app-full){
  padding:0;
  overflow:hidden;
  display:flex;
  flex-direction:column;
  min-height:0;
}

.card-body:has(> .case-body > .cb-app-full) > .case-body{
  flex:1;
  min-height:0;
  display:flex;
  flex-direction:column;
  max-width:none;
  margin:0;
}

.card-body:has(> .case-body > .cb-app-full) .cb-app-full{
  flex:1;
  min-height:0;
  margin:0;
  display:flex;
}

/* The ratio is what the box no longer needs: it fills the card instead. This
   overrides the inline --app-aspect, which is why app_render() emits a variable
   rather than the property itself. No border either — the card already has one,
   and a second line a pixel inside it just reads as a mistake. */
.card-body:has(> .case-body > .cb-app-full) .cb-app-full > .app-frame{
  flex:1;
  aspect-ratio:auto;
  height:auto;
  min-height:0;
  border:none;
}

.card-body:has(> .case-body > .cb-app-full) > .divider,
.card-body:has(> .case-body > .cb-app-full) > .entry-nav,
.card-body:has(> .case-body > .cb-app-full) > .btn-download{
  display:none;
}

/* A pasted-HTML or inline-SVG centrepiece. Width, ceiling and alignment come
   from the same home_loader_style() as the image and app modes, so the four
   behave identically. An <svg> inside inherits currentColor, which is what lets
   a flat one follow the theme instead of being stuck in one. */
.loader-html{
  margin:0;
  color:var(--fg);
}
.loader-html > svg,
.loader-html > img{
  display:block;
  width:100%;
  height:auto;
}

/* Same pinned-height treatment as .loader-media: never stretched, letterboxed
   by default, cropped only when the Full width preset is chosen. */
.loader-html.has-height{
  overflow:hidden;
  display:flex;
  align-items:center;
  justify-content:center;
}
.loader-html.has-height > svg,
.loader-html.has-height > img{
  height:100%;
  object-fit:contain;
}
.loader-html.has-height.is-full > svg,
.loader-html.has-height.is-full > img{ object-fit:cover; }

/* ---------------------------------------------------------------------------
   Lightbox. A <dialog>, so the page behind is inert and Escape works without a
   listener of ours. Inside it, the row is a real scroller with snap points —
   the swipe, the momentum and the rubber banding are the browser's, which is
   the only way they feel right.
   --------------------------------------------------------------------------- */

.is-openable{ cursor:zoom-in; }

.lightbox{
  width:100vw;
  max-width:100vw;
  height:100dvh;
  max-height:100dvh;
  margin:0;
  padding:0;
  border:none;
  background:transparent;
  overflow:hidden;
  color:var(--fg);
}
.lightbox::backdrop{ background:rgba(0,0,0,.88); }

.lightbox-strip{
  width:100%;
  height:100%;
  display:flex;
  overflow-x:auto;
  overflow-y:hidden;
  scroll-snap-type:x mandatory;
  scrollbar-width:none;
  /* pan-x, not none: horizontal swipes belong to the scroller. A pinch is not
     a pan, so the two-finger handler still sees it. */
  touch-action:pan-x;
  overscroll-behavior:contain;
}
.lightbox-strip::-webkit-scrollbar{ display:none; }

.lightbox-slide{
  flex:0 0 100%;
  height:100%;
  scroll-snap-align:center;
  scroll-snap-stop:always;   /* one picture per flick, never two */
  display:flex;
  align-items:center;
  justify-content:center;
  padding:clamp(1rem,4vw,3rem);
}

.lightbox-img{
  max-width:100%;
  max-height:80dvh;
  object-fit:contain;
  cursor:zoom-in;
  -webkit-user-drag:none;
  user-select:none;
  /* Only the zoom animates. Panning must track the pointer exactly — a
     transition there makes the picture lag behind the hand moving it, and the
     drag handler clears this while dragging for the same reason. */
  transition:transform .18s ease;
}
.lightbox-img.is-zoomed{ cursor:grab; }

/* Zoomed: the strip stops being a scroller, so the wheel and the drag belong to
   the picture. Without this every pan would fight a snap point. */
.lightbox.is-zoomed .lightbox-strip{
  overflow:hidden;
  scroll-snap-type:none;
  touch-action:none;
}

.lightbox-caption{
  position:fixed;
  left:0;
  right:0;
  bottom:2.4rem;
  margin:0;
  padding:0 clamp(1rem,4vw,3rem);
  text-align:center;
  font-family:var(--mono, monospace);
  font-size:.72rem;
  line-height:1.5;
  color:var(--muted);
  pointer-events:none;
}
.lightbox-caption[hidden]{ display:none; }

.lightbox-close{
  position:fixed;
  top:.9rem;
  right:1rem;
  z-index:2;
  width:2.1rem;
  height:2.1rem;
  display:flex;
  align-items:center;
  justify-content:center;
  font-size:.9rem;
  color:var(--fg);
  background:transparent;
  border:1px solid var(--muted);
  cursor:pointer;
}
.lightbox-close:hover{ border-color:var(--fg); }

.lightbox-nav{
  position:fixed;
  top:50%;
  transform:translateY(-50%);
  z-index:2;
  width:2.6rem;
  height:3.4rem;
  display:flex;
  align-items:center;
  justify-content:center;
  font-size:1.5rem;
  line-height:1;
  color:var(--fg);
  background:rgba(0,0,0,.35);
  border:1px solid var(--muted);
  cursor:pointer;
}
.lightbox-nav:hover{ border-color:var(--fg); background:rgba(0,0,0,.6); }
.lightbox-nav[hidden]{ display:none; }
.lightbox-nav:disabled{ opacity:.25; cursor:default; }
.lightbox-prev{ left:clamp(.4rem,2vw,1.4rem); }
.lightbox-next{ right:clamp(.4rem,2vw,1.4rem); }
/* Hidden while zoomed — stepping to the next picture mid-zoom is never what
   was meant, and they sit on top of the part you just magnified. */
.lightbox.is-zoomed .lightbox-nav{ display:none; }

.lightbox-hint{
  position:fixed;
  left:0;
  right:0;
  bottom:.9rem;
  margin:0;
  text-align:center;
  font-family:var(--mono, monospace);
  font-size:.64rem;
  letter-spacing:.06em;
  color:var(--muted);
  pointer-events:none;
}

@media (prefers-reduced-motion: reduce){
  .lightbox-img{ transition:none; }
  .lightbox-strip{ scroll-behavior:auto; }
}
