/* ===========================================================================
 * Website Builder — INTERIOR design system
 * ---------------------------------------------------------------------------
 * The ONE stylesheet every native interior page shares (image detail, gallery
 * listing, artist profile, journal post). Loaded by renderThemeInterior()
 * inside the theme shell, so [data-theme]/[data-framework] and the theme token
 * surface are in scope.
 *
 * RULES (keep interiors clean + consistent, no legacy bleedover):
 *   - Style ONLY through theme tokens: --accent, --bg, --bg-mid, --text,
 *     --muted, --rule, --font-display, --font-body, --btn-radius/-tracking/
 *     -transform/-weight. Never hardcode a colour or font here.
 *   - Every interior component composes from these `.ti-*` primitives — one
 *     source of truth, so the four page types can't visually drift.
 *   - No `--color-*` legacy tokens, no `gallery-detail-*`, no `<Nav>`.
 * ======================================================================== */

.ti {
  max-width: 1120px;
  margin: 0 auto;
  padding: 28px 20px 72px;
  color: var(--text);
  background: var(--bg);
  font-family: var(--font-body);
  line-height: 1.55;
}

/* --- Breadcrumb ---------------------------------------------------------- */
.ti-crumbs {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 8px;
  font-size: 13px;
  color: var(--muted);
  margin: 0 0 20px;
}
.ti-crumbs a { color: var(--muted); text-decoration: none; }
.ti-crumbs a:hover { color: var(--accent-text); }
.ti-crumbs__sep { opacity: 0.5; }
.ti-crumbs__current { color: var(--text); }

/* --- Header block (eyebrow / title / byline) ---------------------------- */
.ti-header { margin: 0 0 22px; }
/* "Subscribe via RSS" under the journal heading. Muted like a crumb, accent on
 * hover like a crumb -- it is a utility, not a call to action. */
.ti-feed { margin: 10px 0 0; font-size: 13px; }
.ti-feed a { color: var(--muted); text-decoration: none; }
.ti-feed a:hover { color: var(--accent-text); }
.ti-eyebrow {
  font-size: 12px;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--accent-text);
  margin: 0 0 10px;
}
.ti-title {
  font-family: var(--font-display);
  color: var(--text);
  font-size: clamp(28px, 5vw, 46px);
  line-height: 1.05;
  margin: 0 0 10px;
}
.ti-byline { font-size: 15px; color: var(--muted); margin: 0; }
.ti-byline a { color: var(--accent-text); text-decoration: none; }
.ti-byline a:hover { text-decoration: underline; text-underline-offset: 2px; }

/* --- Media -------------------------------------------------------------- */
.ti-figure {
  margin: 0 0 24px;
  border: 1px solid var(--rule);
  background: var(--bg-mid);
  border-radius: 4px;
  overflow: hidden;
}
.ti-figure img { display: block; width: 100%; height: auto; }
/* Video posts render a real <video> here (2026-08-03) — same box as the
   image so the figure doesn't jump between media types. */
.ti-figure video { display: block; width: 100%; height: auto; background: #000; }

/* 🚨 The height cap on gallery detail media. Without it media is sized purely
   by the column: at .ti's ~1118px a 9:16 reel (the Instagram default, and most
   of what tenants ingest) computes to ~1990px tall, so it overflowed the
   viewport and the visitor landed mid-video. Reported on bobparrtattoos
   2026-09-03; measured there at 1118x1988, cut to 1118x850.

   🚨 Cap HEIGHT, keep `width: 100%`. The tidier-looking alternative --
   `width: auto` + `max-width: 100%`, so the element hugs its ratio and never
   letterboxes -- is wrong here, and measurably: on prod 28,965 of the 29,881
   published images with a known width (96.9%) are NARROWER than this column,
   so it would stop upscaling them and shrink nearly every gallery image on the
   network. CF's `full` variant is fit=scale-down and cannot supply the missing
   pixels either. Keeping width definite also keeps the box reservable -- the
   width/height attrs ThemeImageDetail emits "for CLS" only reserve space while
   one dimension is definite -- so it would have traded overflow for a shift.

   The letterbox is therefore deliberate, and is already this file's
   convention: `.ti-figure video` declares `background: #000` as exactly "the
   video letterbox ground".

   🚨 85vh, not the legacy path's 75vh (app/gallery/image-detail.tsx): the same
   cap is a harder constraint on a wider column, and that path's figure is
   `max-width: 960px` against this one's ~1118px. At 1118 an ordinary 3:2
   landscape is 745px tall, which 75vh clamps on a 1440x900 laptop (675) and
   85vh does not (765) -- while the 9:16 reel is still cut to 765. On a
   viewport under ~800px tall a tall-ish landscape still picks up a small
   letterbox; that is the cap working, since it is already near full-screen.

   Direct children only, .ti-image-detail only: `>` skips CarouselViewer's
   `.carousel-media` (own cap) and the <img> inside <video>; the prefix keeps
   `.ti-portrait` (aspect-ratio:1/1 + cover) out of scope.
   769px, not 768: the phone reorder block below is `max-width: 768px`, and
   768 would apply both at exactly 768. */
@media (min-width: 769px) {
  .ti-image-detail .ti-figure > img,
  .ti-image-detail .ti-figure > video {
    max-height: 85vh;
    object-fit: contain;
  }
}
.ti-figcaption { padding: 12px 16px; font-size: 13px; color: var(--muted); }

/* --- Prose / article body ----------------------------------------------- */
/* The shared long-form primitive: bios, empty-states, AND journal post
 * bodies (dangerouslySetInnerHTML) all compose from this, so editorial
 * content can't visually drift from the rest of the interior system. */
.ti-prose { max-width: 68ch; color: var(--text); line-height: 1.75; }
.ti-prose p { margin: 0 0 1.15em; color: var(--muted); }
.ti-prose a { color: var(--accent-text); }
.ti-prose strong { color: var(--text); }
.ti-prose h2,
.ti-prose h3 {
  font-family: var(--font-display);
  color: var(--text);
  line-height: 1.2;
  margin: 1.6em 0 0.5em;
}
.ti-prose h2 { font-size: 1.5rem; }
.ti-prose h3 { font-size: 1.2rem; }
.ti-prose img {
  max-width: 100%;
  height: auto;
  border-radius: 4px;
  margin: 1.4em 0;
  display: block;
}
.ti-prose blockquote {
  border-left: 3px solid var(--accent);
  margin: 1.4em 0;
  padding: 0.4em 0 0.4em 1.2em;
  color: var(--muted);
  font-style: italic;
}
.ti-prose ul,
.ti-prose ol { margin: 0 0 1.15em; padding-left: 1.4em; color: var(--muted); }
.ti-prose li { margin: 0 0 0.4em; }
/* The MARKERS. Tailwind preflight ships `ol, ul, menu { list-style: none }`,
 * so the padding-left above was indenting a bullet that never rendered — an
 * authored list came out as unmarked lines on every themed tenant. Measured
 * 2026-09-01 alongside the same defect on the non-themed `.blog-body` path.
 * Nesting reads by marker shape as well as indent, because Quill emits a
 * nested level as `li.ql-indent-N` inside one flat list at least as often as
 * it emits a real child list. */
.ti-prose ul { list-style-type: disc; }
.ti-prose ol { list-style-type: decimal; }
.ti-prose ul ul { list-style-type: circle; }
.ti-prose ul ul ul { list-style-type: square; }
.ti-prose ol ol { list-style-type: lower-alpha; }
.ti-prose ol ol ol { list-style-type: lower-roman; }
.ti-prose li > ul,
.ti-prose li > ol { margin-top: 0.35em; margin-bottom: 0; }
/* H4 was unsized, so preflight's `font-size: inherit` left it at body size --
 * a heading that reads as a mistake. Same reason the h2/h3 rules above exist. */
.ti-prose h4 {
  font-family: var(--font-display);
  color: var(--text);
  line-height: 1.2;
  margin: 1.6em 0 0.5em;
  font-size: 1.05rem;
  letter-spacing: 0.01em;
}
/* Quill's alignment + indent classes, which only ever had CSS inside
 * `.ql-editor`: the editor honoured them and the published page dropped them,
 * so centring or indenting looked like it worked and then did nothing live. */
.ti-prose .ql-align-center { text-align: center; }
.ti-prose .ql-align-right { text-align: right; }
.ti-prose .ql-align-justify { text-align: justify; }
/* A bullet indented with Quill's indent button stays in the same flat <ul>,
 * so the `ul ul` shapes above never see it. Stepped by hand to match.
 * NOT done for <ol> — the native counter keeps running, so a stepped ordered
 * marker publishes 1, b, c, 4. See app/globals.css for the full reasoning. */
.ti-prose ul > li.ql-indent-1 { list-style-type: circle; }
.ti-prose ul > li.ql-indent-2 { list-style-type: square; }
.ti-prose .ql-indent-1 { margin-left: 3em; }
.ti-prose .ql-indent-2 { margin-left: 6em; }
.ti-prose .ql-indent-3 { margin-left: 9em; }
.ti-prose .ql-indent-4 { margin-left: 12em; }
.ti-prose .ql-indent-5 { margin-left: 15em; }
.ti-prose .ql-indent-6 { margin-left: 18em; }
.ti-prose .ql-indent-7 { margin-left: 21em; }
.ti-prose .ql-indent-8 { margin-left: 24em; }
/* Empty paragraphs. Quill writes a blank line as `<p><br></p>`; the editor
 * charges 18px for it and this page charges ~69px, so an author chasing a
 * small breath between paragraphs ends up with a chasm. Base rule is
 * universally supported; the `:has()` rule hands a small beat back where it
 * is supported, and is kept separate so old browsers drop only that half.
 * See app/globals.css for the measurements. */
.ti-prose p > br:only-child { display: none; }
.ti-prose p:has(> br:only-child) { margin: 0; height: 0.55em; }
/* Figures from the editor's image blot (G-3). Same data attributes as the
 * non-themed surfaces in app/globals.css; theme tokens for the caption. */
.ti-prose figure.ql-figure { margin: 1.5em auto; max-width: 100%; }
.ti-prose figure.ql-figure[data-width="medium"] { max-width: 66%; }
.ti-prose figure.ql-figure[data-width="small"] { max-width: 40%; }
.ti-prose figure.ql-figure[data-align="left"] { float: left; margin: 0.35em 1.5em 0.75em 0; }
.ti-prose figure.ql-figure[data-align="right"] { float: right; margin: 0.35em 0 0.75em 1.5em; }
.ti-prose figure.ql-figure img { display: block; width: 100%; height: auto; margin: 0; border-radius: 4px; }
.ti-prose figure.ql-figure figcaption { font-size: 13px; line-height: 1.5; text-align: center; color: var(--muted); padding: 8px 4px 0; }
.ti-prose figure.ql-figure figcaption:empty { display: none; }
/* Contain the float rather than clearing the last child -- clearing pushed a
 * trailing paragraph below the image so it never wrapped, and still let a
 * trailing figure escape the container. See app/globals.css. */
.ti-prose { display: flow-root; }
/* Safari 12 (the kiosk-iPad floor pinned in package.json) has no flow-root. */
.ti-prose::after { content: ''; display: block; clear: both; }
/* flow-root stops parent/child margin collapsing, so the first heading and the
 * last paragraph gain their own margin against the container edge. The globals
 * surfaces already carry these resets; .ti-prose did not. */
.ti-prose > :first-child { margin-top: 0; }
.ti-prose > :last-child { margin-bottom: 0; }
@media (max-width: 640px) {
  .ti-prose figure.ql-figure { float: none !important; max-width: 100% !important; margin: 1.5em auto !important; }
}
.ti-prose pre {
  margin: 0 0 1.15em;
  padding: 0.9em 1.1em;
  border-radius: 4px;
  /* The theme's own secondary surface, not a hardcoded tint: this stylesheet
   * serves light themes and dark ones, and either fixed grey is wrong on one
   * of them. `--rule` draws the edge so the block still reads on a theme whose
   * --bg-mid equals its --bg. */
  background: var(--bg-mid, transparent);
  border: 1px solid var(--rule, transparent);
  color: var(--muted);
  font-size: 0.9em;
  line-height: 1.5;
  white-space: pre-wrap;
  overflow-x: auto;
}
.ti-prose hr {
  margin: 2em 0;
  border: 0;
  border-top: 1px solid var(--rule, currentColor);
  opacity: 0.5;
}

/* --- Meta chips (styles, date, etc.) ------------------------------------ */
.ti-meta { display: flex; flex-wrap: wrap; gap: 8px; margin: 20px 0; }
.ti-chip {
  font-size: 12px;
  letter-spacing: 0.04em;
  padding: 4px 10px;
  border: 1px solid var(--rule);
  border-radius: 999px;
  color: var(--muted);
  text-decoration: none;
}
.ti-chip:hover { color: var(--accent-text); border-color: var(--accent); }

/* --- Pager (prev / next) ------------------------------------------------ */
.ti-pager {
  display: grid;
  grid-template-columns: 1fr auto 1fr;
  align-items: center;
  gap: 12px;
  margin: 24px 0;
}
.ti-pager__prev { justify-self: start; }
.ti-pager__next { justify-self: end; }
.ti-pager__mid { justify-self: center; }
.ti-pglink {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  color: var(--text);
  text-decoration: none;
  font-size: 14px;
  /* 44x44 is the Apple HIG minimum (Material asks 48dp). This was
     `padding: 8px 4px` at 14px — about 34px tall, and the prev/next links are
     the primary way to move through a gallery on a phone. */
  min-height: 44px;
  padding: 8px 12px;
}
.ti-pglink:hover { color: var(--accent-text); }
.ti-pglink[aria-disabled='true'] { opacity: 0.35; pointer-events: none; }

/* --- Numbered pagination (gallery listing) -------------------------------
   GalleryPagination variant="ti": the windowed 1 … n-1 [n] n+1 … N bar the
   network gallery has always had, restyled through theme tokens for the
   Website Builder interiors. Active page inverts onto the accent — same
   on-accent ink rule as the section system's --accent-contrast usage.
   44px minimum tap targets. */
.ti-pagenav {
  display: flex;
  justify-content: center;
  align-items: center;
  flex-wrap: wrap;
  gap: 6px;
  margin: 28px 0 0;
  font-family: var(--font-body);
  font-size: 14px;
}
.ti-pagenum {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 44px;
  min-height: 44px;
  padding: 8px 10px;
  border: 1px solid var(--rule);
  border-radius: var(--btn-radius, 6px);
  color: var(--text);
  text-decoration: none;
  transition: color 0.15s ease, border-color 0.15s ease;
}
.ti-pagenum:hover { color: var(--accent-text); border-color: var(--accent); }
.ti-pagenum--active {
  background: var(--accent);
  color: var(--accent-contrast, var(--bg));
  border-color: var(--accent);
  font-weight: 600;
}
.ti-pagenum--active:hover { color: var(--accent-contrast, var(--bg)); }
.ti-pagenav__gap { color: var(--muted); padding: 0 2px; }

/* --- Table of contents --------------------------------------------------
   Rendered by components/blog/BlogToc.tsx with variant="theme". */
.ti-toc {
  margin: 0 0 28px;
  padding: 14px 18px;
  border-left: 2px solid var(--accent);
  background: var(--bg-mid, rgba(127, 127, 127, 0.06));
  border-radius: 0 6px 6px 0;
}
.ti-toc__h {
  margin: 0 0 8px;
  font-family: var(--font-body);
  font-size: 11px;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--muted);
}
.ti-toc__list { margin: 0; padding: 0; list-style: none; }
.ti-toc__list li { margin: 0 0 5px; line-height: 1.4; }
.ti-toc__list li:last-child { margin-bottom: 0; }
.ti-toc__list a { font-family: var(--font-body); font-size: 14px; color: var(--text); text-decoration: none; }
.ti-toc__list a:hover { color: var(--accent-text); text-decoration: underline; }
.ti-toc__sub { padding-left: 16px; }
.ti-toc__sub a { font-size: 13px; color: var(--muted); }
/* Fragment jumps land under the theme's fixed nav rather than behind it. */
.ti-prose h2, .ti-prose h3 { scroll-margin-top: 90px; }

/* --- Related posts ------------------------------------------------------
   Rendered by components/blog/BlogRelated.tsx with variant="theme". */
.ti-related { margin: 32px 0 0; }
.ti-related__h {
  font-family: var(--font-display);
  font-size: 18px;
  color: var(--text);
  margin: 0 0 14px;
}
.ti-related__grid {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  gap: 14px;
  grid-template-columns: repeat(auto-fill, minmax(min(220px, 100%), 1fr));
}
.ti-related__card {
  display: flex;
  flex-direction: column;
  gap: 6px;
  height: 100%;
  padding: 10px;
  border: 1px solid var(--rule);
  border-radius: var(--btn-radius, 8px);
  text-decoration: none;
  transition: border-color 0.15s ease, transform 0.15s ease;
}
.ti-related__card:hover { border-color: var(--accent); transform: translateY(-2px); }
.ti-related__thumb {
  width: 100%;
  aspect-ratio: 16 / 10;
  object-fit: cover;
  border-radius: 5px;
  display: block;
}
.ti-related__t {
  font-family: var(--font-display);
  font-size: 15px;
  line-height: 1.35;
  color: var(--text);
}
.ti-related__card:hover .ti-related__t { color: var(--accent-text); }
.ti-related__d { font-size: 12px; color: var(--muted); }

/* --- Post nav (prev / next blog post) -----------------------------------
   Richer than .ti-pager on purpose: a blog neighbor needs its TITLE and DATE
   on the card, the way the /support Knowledge Center does it. Rendered by
   components/blog/BlogPostNav.tsx with variant="theme". */
.ti-postnav {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 12px;
  margin: 32px 0 24px;
}
.ti-postnav__link {
  display: flex;
  flex-direction: column;
  gap: 4px;
  min-height: 44px;
  padding: 14px 16px;
  border: 1px solid var(--rule);
  border-radius: var(--btn-radius, 8px);
  text-decoration: none;
  transition: border-color 0.15s ease, transform 0.15s ease;
}
.ti-postnav__link:hover { border-color: var(--accent); transform: translateY(-2px); }
.ti-postnav__next { text-align: right; align-items: flex-end; }
.ti-postnav__label {
  font-family: var(--font-body);
  font-size: 12px;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--accent-text);
}
.ti-postnav__title {
  font-family: var(--font-display);
  font-size: 16px;
  line-height: 1.3;
  color: var(--text);
}
.ti-postnav__link:hover .ti-postnav__title { color: var(--accent-text); }
.ti-postnav__date { font-size: 12px; color: var(--muted); }
@media (max-width: 560px) {
  .ti-postnav { grid-template-columns: 1fr; }
  .ti-postnav__next { text-align: left; align-items: flex-start; }
}

/* --- Buttons (share the theme's button shape tokens) -------------------- */
.ti-btn {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  padding: 11px 22px;
  font-family: var(--font-body);
  font-weight: var(--btn-weight, 600);
  letter-spacing: var(--btn-tracking, 0.04em);
  text-transform: var(--btn-transform, none);
  border-radius: var(--btn-radius, 4px);
  text-decoration: none;
  cursor: pointer;
  border: 1px solid transparent;
}
.ti-btn--primary { background: var(--accent); color: var(--bg); }
.ti-btn--primary:hover { filter: brightness(1.08); }
.ti-btn--ghost { background: transparent; color: var(--text); border-color: var(--rule); }
.ti-btn--ghost:hover { border-color: var(--accent); color: var(--accent-text); }

/* --- Grid (gallery listing / related) ----------------------------------- */
.ti-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
  gap: 12px;
}
/* 🚨 `display: block` below overrides the `hidden` attribute, so the chip
   filter (GalleryChipsClient sets tile.hidden) could not hide anything on the
   artist page. Found in review 2026-08-31 alongside the selector bug. */
.ti-grid__tile[hidden] { display: none; }
.ti-grid__tile {
  display: block;
  /* positioned: MediaBadge (video ▶ / carousel count) overlays the tile */
  position: relative;
  border: 1px solid var(--rule);
  background: var(--bg-mid);
  border-radius: 4px;
  overflow: hidden;
  aspect-ratio: 1 / 1;
}
.ti-grid__tile img {
  width: 100%; height: 100%; object-fit: cover; display: block;
  transition: filter 0.35s ease, transform 0.35s ease;
}

/* Rollover: the photo falls back and the piece's own title comes forward in
   the theme's display face (operator 2026-08-31). Gated three ways —
   `--titled` so a tile with no title never blurs to reveal nothing,
   `hover: hover` so a phone tap does not leave a tile stuck blurred, and
   reduced-motion so the reveal is instant rather than animated. */
@media (hover: hover) {
  .ti-grid__tile--titled:hover img { filter: blur(6px) brightness(0.5); transform: scale(1.05); }
  .ti-grid__tile--titled:hover .ti-grid__cap { opacity: 1; }
}
.ti-grid__tile--titled:focus-visible img { filter: blur(6px) brightness(0.5); transform: scale(1.05); }
.ti-grid__tile--titled:focus-visible .ti-grid__cap { opacity: 1; }
.ti-grid__cap {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 16px;
  opacity: 0;
  transition: opacity 0.28s ease;
  pointer-events: none;
}
.ti-grid__cap-text {
  font-family: var(--font-display);
  font-size: clamp(14px, 1.5vw, 19px);
  line-height: 1.25;
  letter-spacing: 0.04em;
  color: var(--text);
  text-align: center;
  text-wrap: balance;
  /* The blur sits behind the words, but a light image can still ride up
     under them — a soft shadow keeps the title legible on any photo. */
  text-shadow: 0 2px 14px rgba(0, 0, 0, 0.75);
}
@media (prefers-reduced-motion: reduce) {
  .ti-grid__tile img,
  .ti-grid__cap { transition: none; }
  .ti-grid__tile--titled:hover img { transform: none; }
}

/* --- Expanded gallery treatments (SECTION-VARIANT-CANON V-3 / C-4) -------
   Map + rationale: lib/themes/interior/expanded.ts. Scoped to artist-work so
   the shared .ti-grid primitive on /gallery is not repainted. */
[data-section="artist-work"][data-treatment="hover"] .ti-grid__tile img {
  transition: transform 0.35s ease;
}
[data-section="artist-work"][data-treatment="hover"] .ti-grid__tile:hover img,
[data-section="artist-work"][data-treatment="hover"] .ti-grid__tile:focus-visible img {
  transform: scale(1.06);
}
/* Applied only inside no-preference: the tiles must stay visible when motion
   is reduced, not be animated and then cancelled. */
@media (prefers-reduced-motion: no-preference) {
  [data-section="artist-work"][data-treatment="stagger"] .ti-grid__tile {
    animation: ti-stagger-in 0.5s ease both;
  }
  @keyframes ti-stagger-in {
    from { opacity: 0; transform: translateY(10px); }
    to   { opacity: 1; transform: none; }
  }
  /* Stepped in bands, not per tile: a 45ms-per-tile cascade over 12 tiles is
     not what the eye reads, and 12 rules ship on every interior page. */
  [data-section="artist-work"][data-treatment="stagger"] .ti-grid__tile:nth-child(n + 4) { animation-delay: 90ms; }
  [data-section="artist-work"][data-treatment="stagger"] .ti-grid__tile:nth-child(n + 8) { animation-delay: 180ms; }
  [data-section="artist-work"][data-treatment="stagger"] .ti-grid__tile:nth-child(n + 12) { animation-delay: 270ms; }
}
[data-section="artist-work"][data-treatment="bento"] .ti-grid { grid-auto-flow: dense; }
@media (min-width: 620px) {
  [data-section="artist-work"][data-treatment="bento"] .ti-grid__tile:nth-child(7n + 1) {
    grid-column: span 2;
    grid-row: span 2;
    aspect-ratio: auto;
  }
}
[data-section="artist-work"][data-treatment="portrait"] .ti-grid {
  grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
}
[data-section="artist-work"][data-treatment="portrait"] .ti-grid__tile { aspect-ratio: 3 / 4; }
[data-section="artist-work"][data-treatment="editorial"] .ti-grid {
  grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
  gap: 20px;
}
[data-section="artist-work"][data-treatment="editorial"] .ti-grid__tile {
  aspect-ratio: 4 / 5;
  border-radius: 2px;
}

/* Artist intro treatments (roster family). Map: lib/themes/interior/expanded.ts */
[data-section="artist-intro"][data-treatment="glow"] .ti-portrait img {
  transition: box-shadow 0.3s ease;
}
[data-section="artist-intro"][data-treatment="glow"] .ti-portrait:hover img {
  box-shadow: 0 0 0 3px var(--accent);
}
/* Beats .ti-portrait--round, which the stacked intro hardcodes — the square
   treatment exists precisely to undo it. */
[data-section="artist-intro"][data-treatment="square"] .ti-portrait--round,
[data-section="artist-intro"][data-treatment="square"] .ti-portrait--round img {
  border-radius: 4px;
}

/* Ticket treatments (pricing family). One structure, so these are looks. */
[data-section="event-tickets"][data-treatment="cards"] .ti-tickets,
[data-section="event-tickets"][data-treatment="lift"] .ti-tickets {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
  gap: 14px;
}
[data-section="event-tickets"][data-treatment="lift"] .ti-ticket {
  transition: transform 0.2s ease;
}
[data-section="event-tickets"][data-treatment="lift"] .ti-ticket:hover {
  transform: translateY(-3px);
}
[data-section="event-tickets"][data-treatment="table"] .ti-tickets {
  display: table;
  width: 100%;
}
[data-section="event-tickets"][data-treatment="table"] .ti-ticket {
  display: table-row;
}

/* Lineup treatments (roster family, teaser density). Map: expanded.ts */
[data-section="event-lineup"][data-treatment="round"] .ti-lineup__photo {
  border-radius: 999px;
}
[data-section="event-lineup"][data-treatment="square"] .ti-lineup__photo {
  border-radius: 2px;
}
[data-section="event-lineup"][data-treatment="glow"] .ti-lineup__photo {
  transition: box-shadow 0.3s ease;
}
[data-section="event-lineup"][data-treatment="glow"] .ti-lineup__item:hover .ti-lineup__photo {
  box-shadow: 0 0 0 3px var(--accent);
}

/* --- Journal cards (blog index) ------------------------------------------ */
.ti-cards {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
  gap: 14px;
}
.ti-card {
  display: flex;
  flex-direction: column;
  border: 1px solid var(--rule);
  background: var(--bg-mid);
  border-radius: 4px;
  overflow: hidden;
  text-decoration: none;
}
.ti-card__media { aspect-ratio: 16 / 9; overflow: hidden; background: var(--bg-mid); }
.ti-card__media img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
  transition: transform 0.35s ease;
}
.ti-card:hover .ti-card__media img { transform: scale(1.04); }
.ti-card__body { padding: 18px; display: flex; flex-direction: column; flex: 1; }
.ti-card__date {
  font-size: 12px;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--muted);
  margin: 0 0 8px;
}
.ti-card__title {
  font-family: var(--font-display);
  font-size: 19px;
  line-height: 1.25;
  color: var(--text);
  margin: 0 0 8px;
}
.ti-card__excerpt { font-size: 14px; line-height: 1.6; color: var(--muted); margin: 0; flex: 1; }
.ti-card__more {
  font-size: 12px;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--accent-text);
  margin-top: 14px;
}

/* --- Section head (title + trailing link) ------------------------------- */
.ti-secthead {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 16px;
  margin: 44px 0 16px;
}
.ti-secthead h2 {
  font-family: var(--font-display);
  color: var(--text);
  font-size: clamp(20px, 3vw, 28px);
  line-height: 1.1;
  margin: 0;
}
.ti-secthead__link {
  font-size: 13px;
  color: var(--accent-text);
  text-decoration: none;
  white-space: nowrap;
}
.ti-secthead__link:hover { text-decoration: underline; text-underline-offset: 2px; }

/* --- Artist portrait (aside of .ti-detail) ------------------------------ */
.ti-portrait { margin: 0; }
.ti-portrait img { aspect-ratio: 1 / 1; object-fit: cover; }
/* 🚨 On a phone the portrait comes FIRST. The DOM order is text-then-photo
   because the desktop grid wants the bio in the left column and the portrait
   in the right; when that grid collapses to one column the portrait fell
   BELOW the whole bio, so an artist page opened on a face-less wall of text
   and you had to scroll past it to find out who you were reading about.
   Reported by a client: "Artist bio pages on mobile should have artist pic
   above the text, currently under" (Sean, Darkside, 2026-08-14).

   Fixed with `order`, not by moving the markup: the DOM order is correct for
   desktop and for a screen reader (name and bio before a decorative
   portrait), and reordering the source would have to be undone at 860px. */
@media (max-width: 859px) {
  .ti-detail { display: flex; flex-direction: column; }
  .ti-detail > .ti-portrait { order: -1; margin-bottom: 20px; }
}

/* --- Layout helpers ----------------------------------------------------- */
.ti-actions { display: flex; flex-wrap: wrap; gap: 12px; margin: 8px 0 24px; }
@media (min-width: 860px) {
  .ti-detail { display: grid; grid-template-columns: minmax(0, 1.4fr) minmax(0, 1fr); gap: 32px; align-items: start; }
}

/* --- Image detail: parity blocks (2026-08-25) ----------------------------
   Publish date, description/caption prose, share + IG source, and the
   Explore keyword chips — the details the themed page used to drop vs the
   legacy gallery detail. All token-pure per the interior rules. */
.ti-detail-date {
  font-size: 12px;
  letter-spacing: 0.05em;
  text-transform: uppercase;
  color: var(--muted);
  margin: 12px 0 0;
}
.ti-detail-desc { margin: 18px 0 0; }
.ti-detail-desc .desc-ext-link {
  display: inline-block;
  font-size: 0.75rem;
  font-weight: 600;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  text-decoration: none;
  padding: 0.55rem 1.25rem;
  border: 1px solid currentColor;
  border-radius: 2px;
  margin-right: 0.5rem;
}
.ti-share { margin: 20px 0 0; }
.ti-source { margin: 10px 0 0; font-size: 12px; }
.ti-source a { color: var(--muted); text-decoration: none; }
.ti-source a:hover { color: var(--accent-text); }
.ti-explore { margin: 32px 0 0; }
.ti-explore__h {
  font-family: var(--font-display);
  color: var(--text);
  font-size: 15px;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  margin: 0 0 12px;
}
.ti-explore__groups {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 8px 18px;
}
.ti-explore__group {
  display: inline-flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 6px;
}
.ti-explore__label {
  font-size: 11px;
  font-weight: 600;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--muted);
}

/* --- FAQ (image-detail fact-derived Q&A) --------------------------------- */
.ti-faq { margin: 40px 0 0; padding-top: 24px; border-top: 1px solid var(--rule); }
.ti-faq__item + .ti-faq__item { margin-top: 18px; }
.ti-faq__question {
  font-family: var(--font-display);
  color: var(--text);
  font-size: 17px;
  line-height: 1.3;
  margin: 0 0 6px;
}
.ti-faq__answer { font-size: 14px; line-height: 1.6; color: var(--muted); margin: 0; }

/* --- Image detail: phone layout ----------------------------------------
   On a phone the photo IS the page, so it comes directly under the
   breadcrumb; title, byline and CTA follow it. Done with flex `order`, not by
   reordering the DOM, so the H1 stays early in the document for SEO and
   screen-reader order (operator 2026-07-26: "on mobile, h1 can be below
   fold"). Safe for tab order too — the figure contains no focusable elements,
   so the relative order of every link on the page is unchanged.

   Before this, ~59% of a 390x844 viewport was chrome before the image. */
@media (max-width: 768px) {
  .ti-image-detail {
    display: flex;
    flex-direction: column;
  }
  .ti-image-detail > .ti-crumbs { order: 0; }
  .ti-image-detail > [data-swipe-zone],
  .ti-image-detail > .ti-figure { order: 1; }
  .ti-image-detail > .ti-header { order: 2; margin-top: 4px; }
  .ti-image-detail > .ti-actions { order: 3; }
  .ti-image-detail > .ti-detail-date { order: 4; }
  .ti-image-detail > .ti-pager { order: 5; }
  .ti-image-detail > .ti-detail-desc { order: 6; }
  .ti-image-detail > .ti-share { order: 7; }
  .ti-image-detail > .ti-explore { order: 8; }
  .ti-image-detail > .ti-faq { order: 9; }
}


/* ============================================================
   /artists — studio roster page (ThemeArtistRoster).
   Uses the same .ti-* tokens as the rest of the interior pages so it
   inherits the tenant's theme with no bespoke design.
   ============================================================ */
.ti-roster {
  list-style: none;
  margin: 0 0 32px;
  padding: 0;
  display: grid;
  /* auto-fit + minmax so a 3-person shop and a 15-person shop both look
     deliberate without a breakpoint per size. */
  grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
  gap: 28px 24px;
}
.ti-roster__link {
  display: block;
  text-decoration: none;
  color: inherit;
  transition: transform 0.18s ease;
}
.ti-roster__link:hover { transform: translateY(-2px); }
.ti-roster__link--static { cursor: default; }
.ti-roster__photo {
  width: 100%;
  aspect-ratio: 1 / 1;
  overflow: hidden;
  border-radius: 50%;
  background: var(--bg-mid, rgba(255,255,255,0.06));
  display: flex;
  align-items: center;
  justify-content: center;
  margin-bottom: 14px;
}
.ti-roster__photo img { width: 100%; height: 100%; object-fit: cover; display: block; }
.ti-roster__initials {
  font-family: var(--font-display, serif);
  font-size: 30px;
  color: var(--muted, #999);
}
.ti-roster__name {
  font-family: var(--font-display, serif);
  font-size: 20px;
  margin: 0 0 4px;
  text-align: center;
}
.ti-roster__roles,
.ti-roster__styles {
  margin: 0 0 4px;
  text-align: center;
  font-size: 12px;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--muted, #999);
}
.ti-roster__roles { color: var(--accent-text, #c9a227); }
.ti-roster__bio {
  margin: 8px 0 0;
  text-align: center;
  font-size: 14px;
  line-height: 1.5;
  color: var(--muted, #999);
}
.ti-roster__foot {
  text-align: center;
  color: var(--muted, #999);
  font-size: 14px;
}
@media (prefers-reduced-motion: reduce) {
  .ti-roster__link { transition: none; }
  .ti-roster__link:hover { transform: none; }
}

/* --- Upcoming appearances (artist-events slot) --------------------------
   The `events` family at teaser density, scoped to one artist. Tokens only —
   no literal colours — so every theme dresses it the same way it dresses the
   homepage events widget. */
.ti-events {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 1px;
  background: var(--rule);
  border: 1px solid var(--rule);
  border-radius: 6px;
  overflow: hidden;
}
.ti-events__row { margin: 0; }
.ti-events__item {
  display: flex;
  align-items: baseline;
  gap: 14px;
  padding: 14px 16px;
  background: var(--bg-mid);
  color: var(--text);
  text-decoration: none;
  font-family: var(--font-body);
  transition: background 0.15s ease;
}
a.ti-events__item:hover { background: var(--bg); }
a.ti-events__item:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: -2px;
}
.ti-events__name {
  font-weight: 600;
  font-size: 15px;
  color: var(--text);
}
.ti-events__meta {
  font-size: 13px;
  color: var(--muted);
}
/* Variant A stacks name over meta on a phone; the row is baseline-aligned on
   wider screens. */
@media (max-width: 560px) {
  .ti-events__item {
    flex-direction: column;
    align-items: flex-start;
    gap: 4px;
  }
}

/* Variant B — date-block rail. */
.ti-events--rail .ti-events__item { align-items: center; gap: 16px; }
.ti-events--rail .ti-events__date {
  flex: none;
  display: grid;
  place-items: center;
  width: 52px;
  padding: 6px 0;
  border: 1px solid var(--rule);
  border-radius: 4px;
  line-height: 1.05;
}
.ti-events__month {
  font-family: var(--font-body);
  font-size: 10px;
  letter-spacing: 0.1em;
  color: var(--accent-text);
  font-weight: 700;
}
.ti-events__day {
  font-family: var(--font-display);
  font-size: 20px;
  color: var(--text);
}
.ti-events__body { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
@media (max-width: 560px) {
  .ti-events--rail .ti-events__item { flex-direction: row; align-items: center; }
}

/* --- Artist intro, variant B (centred stack) ----------------------------
   Variant A's two-column grid leaves an empty column when an artist has no
   photo; this variant is single-column by construction. */
.ti-detail--stacked {
  display: block;
  max-width: 720px;
  margin-inline: auto;
  text-align: center;
}
.ti-header--center,
.ti-meta--center,
.ti-actions--center { text-align: center; justify-content: center; }
.ti-meta--center,
.ti-actions--center { display: flex; flex-wrap: wrap; }
.ti-prose--center { text-align: left; }
.ti-portrait--round {
  width: 168px;
  margin: 0 auto 20px;
}
.ti-portrait--round img {
  aspect-ratio: 1;
  object-fit: cover;
  border-radius: 999px;
}

/* --- Event detail (event-detail slots) ----------------------------------
   Tokens only, so every theme dresses the event page the way it dresses its
   own interiors. */
.ti-note {
  font-family: var(--font-body);
  font-size: 13px;
  color: var(--muted);
  border-left: 2px solid var(--accent);
  padding-left: 12px;
  margin: 20px 0 0;
}

/* Event header, variant B — banner with the title over it. */
.ti-eventbanner {
  position: relative;
  border-radius: 8px;
  overflow: hidden;
  min-height: 260px;
  display: grid;
  place-items: center;
  text-align: center;
  background: var(--bg-mid);
}
.ti-eventbanner__img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  opacity: 0.4;
}
.ti-eventbanner__body { position: relative; padding: 40px 24px; max-width: 720px; }
.ti-eventbanner--flat { min-height: 0; }

/* Lineup, variant A — artist cards. */
.ti-lineup {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
  gap: 10px;
}
.ti-lineup__row { margin: 0; }
.ti-lineup__item {
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 10px 12px;
  border: 1px solid var(--rule);
  border-radius: 6px;
  background: var(--bg-mid);
  color: var(--text);
  text-decoration: none;
  font-family: var(--font-body);
  height: 100%;
}
a.ti-lineup__item:hover { border-color: var(--accent); }
a.ti-lineup__item:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.ti-lineup__photo {
  flex: none;
  width: 46px;
  height: 46px;
  border-radius: 999px;
  object-fit: cover;
  background: var(--bg);
}
.ti-lineup__photo--empty { display: block; border: 1px solid var(--rule); }
.ti-lineup__body { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.ti-lineup__name { font-weight: 600; font-size: 14px; }
.ti-lineup__meta { font-size: 12px; color: var(--muted); }
.ti-lineup__flag {
  margin-left: auto;
  flex: none;
  font-size: 9px;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--accent-text);
  border: 1px solid var(--accent);
  border-radius: 3px;
  padding: 2px 5px;
}

/* Lineup, variant B — dense name list. */
.ti-namelist {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
  gap: 6px 20px;
  font-family: var(--font-body);
}
.ti-namelist__item { display: flex; align-items: baseline; gap: 6px; font-size: 15px; }
.ti-namelist__item a { color: var(--text); text-decoration: none; }
.ti-namelist__item a:hover { color: var(--accent-text); text-decoration: underline; text-underline-offset: 2px; }
.ti-namelist__flag { color: var(--accent-text); font-size: 11px; }

/* Schedule. */
.ti-sched__day { margin-top: 24px; }
.ti-sched__dayhead {
  font-family: var(--font-display);
  font-size: 16px;
  color: var(--accent-text);
  margin: 0 0 10px;
}
.ti-sched { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 1px; }
.ti-sched__row {
  display: flex;
  gap: 16px;
  padding: 12px 14px;
  background: var(--bg-mid);
  border: 1px solid var(--rule);
  border-radius: 6px;
  font-family: var(--font-body);
}
.ti-sched__when {
  flex: none;
  width: 132px;
  font-size: 13px;
  color: var(--muted);
  font-variant-numeric: tabular-nums;
}
.ti-sched__body { display: flex; flex-direction: column; gap: 3px; min-width: 0; }
.ti-sched__title { font-weight: 600; font-size: 15px; color: var(--text); }
.ti-sched__meta { font-size: 12px; color: var(--muted); }
.ti-sched__desc { font-size: 13px; color: var(--muted); line-height: 1.6; margin-top: 4px; }
@media (max-width: 640px) {
  .ti-sched__row { flex-direction: column; gap: 6px; }
  .ti-sched__when { width: auto; }
}

/* Tickets. */
.ti-tickets {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
  gap: 12px;
}
.ti-ticket {
  border: 1px solid var(--rule);
  border-radius: 8px;
  padding: 16px;
  background: var(--bg-mid);
  display: flex;
  flex-direction: column;
  gap: 10px;
  font-family: var(--font-body);
}
.ti-ticket--gone { opacity: 0.6; }
.ti-ticket__head { display: flex; align-items: baseline; justify-content: space-between; gap: 10px; }
.ti-ticket__name { font-weight: 600; font-size: 15px; color: var(--text); }
.ti-ticket__price { font-family: var(--font-display); font-size: 20px; color: var(--accent-text); }
.ti-ticket__was { font-size: 11px; color: var(--muted); text-decoration: line-through; }
.ti-ticket__includes { margin: 0; font-size: 13px; color: var(--muted); line-height: 1.6; }
.ti-ticket__cta { align-self: flex-start; margin-top: auto; }
.ti-ticket__gone {
  align-self: flex-start;
  margin-top: auto;
  font-size: 11px;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--muted);
  border: 1px solid var(--rule);
  border-radius: 3px;
  padding: 4px 8px;
}

/* --- Hero at SLIM density (interior page headers) -------------------------
 *
 * docs/SECTION-VARIANT-CANON.md V-2 + §4a. ThemeShell stamps
 * [data-hero-slim] on the wrapper of every themed interior page; the
 * treatment comes from the DECLARED ladder in lib/themes/hero-slim.ts, so a
 * tenant's interior header wears their own hero, not a platform default.
 *
 * 🚨 Every selector here is [data-hero-slim="x"] .tn-page-header (0,2,0) so
 * it beats PageHeader's own emitted .tn-page-header rules (0,1,0), which
 * render LATER in the document. Specificity is doing the work, not order —
 * see the docblock in components/layout/PageHeader.tsx.
 *
 * 🚨 Every treatment is STILL. The full-size heroes pan, parallax, crossfade
 * and tilt; a 200px band that does any of that sits above someone's reading
 * and competes with it. Slim declines the motion rather than restaging it.
 *
 * Adding a treatment? Add its [data-hero-slim="..."] block here AND map at
 * least one variant to it — scripts/check-hero-slim.mjs fails the push when a
 * mapped treatment has no rules, because a treatment with no stylesheet is
 * indistinguishable from the fallback it was meant to replace.
 * ------------------------------------------------------------------------ */

/* Shared slim geometry: every treatment is a BAND, not a screen. The top
   padding still has to clear the floating nav (~158px desktop), so the saving
   comes out of the bottom and the min-height, not the top. */
[data-hero-slim] .tn-page-header {
  min-height: 240px;
}
[data-hero-slim] .tn-page-header__content {
  padding: 168px 24px 28px;
}
[data-hero-slim] .tn-page-header__title {
  font-size: clamp(24px, 3.2vw, 38px);
}
[data-hero-slim] .tn-page-header__sub {
  font-size: clamp(13px, 1.1vw, 15px);
  margin-top: 10px;
}

/* centered — a/c/d/e/f/g. Image-backed heroes all reduce to this: the banner
   still, the title over it. Deliberately closest to the pre-2026-08-30
   header, because six of the fourteen variants land here and their tenants
   should see a tightening, not a redesign. */
[data-hero-slim="centered"] .tn-page-header__content {
  text-align: center;
}

/* editorial — b/i/l. These heroes are asymmetric; centring them throws away
   the one thing that makes them recognisable. Left-aligned, with the accent
   rule the literary/split heroes carry, and constrained so a long studio name
   does not run the full width. */
[data-hero-slim="editorial"] .tn-page-header {
  justify-content: flex-start;
}
[data-hero-slim="editorial"] .tn-page-header__content {
  text-align: left;
  max-width: var(--max-w, 1100px);
  padding-left: clamp(20px, 6vw, 88px);
}
[data-hero-slim="editorial"] .tn-page-header__title {
  max-width: 18ch;
}
[data-hero-slim="editorial"] .tn-page-header__sub {
  margin-left: 0;
  max-width: 52ch;
}
[data-hero-slim="editorial"] .tn-page-header__eyebrow::after {
  content: "";
  display: block;
  width: 46px;
  height: 2px;
  margin-top: 12px;
  background: var(--accent);
}
[data-hero-slim="editorial"] .tn-page-header__crumbs {
  justify-content: flex-start;
}

/* mark — k/m. Centred, but with breathing room above the title where the
   full-size hero puts its sigil/mark, and the eyebrow tracked out to read as
   an ornament rather than a label. */
[data-hero-slim="mark"] .tn-page-header__content {
  text-align: center;
  padding-top: 176px;
}
[data-hero-slim="mark"] .tn-page-header__eyebrow {
  letter-spacing: 0.42em;
  margin-bottom: 22px;
}
[data-hero-slim="mark"] .tn-page-header__title {
  letter-spacing: 0.01em;
}

/* mosaic — j/n. The full-size heroes are a grid of images; at band height
   there is room for a strip, not a grid. Keep the banner, veil it harder than
   the default so the type stays legible against a busier image, and let a
   hairline separate the band from the page. */
[data-hero-slim="mosaic"] .tn-page-header {
  border-bottom: 1px solid var(--rule);
}
/* 🚨 Veil opacity is a CONTRAST floor, not a taste setting. This treatment
   replaces a guaranteed 55-70% black scrim with a veil toward var(--bg), so
   the H1 (which is var(--text) here, with no text-shadow) is only legible to
   the extent the veil actually covers the photo. 74->86% keeps the effective
   surface within a few percent of the theme's own --bg/--text pair, which
   ThemeShell has already contrast-clamped; the image still reads through as
   texture. Do not lower these without re-scoring the pair. Same principle as
   hero_scrim (CLAUDE.md / BACKGROUND-SYSTEM.md): veiling TOWARD --bg always
   increases contrast against --text, veiling toward anything else does not. */
[data-hero-slim="mosaic"] .tn-page-header[data-banner="1"] .tn-page-header__veil {
  background: linear-gradient(
    180deg,
    color-mix(in srgb, var(--bg) 74%, transparent) 0%,
    color-mix(in srgb, var(--bg) 86%, transparent) 100%
  );
}
[data-hero-slim="mosaic"] .tn-page-header[data-banner="1"] .tn-page-header__title {
  color: var(--text);
  text-shadow: none;
}
[data-hero-slim="mosaic"] .tn-page-header[data-banner="1"] .tn-page-header__sub {
  color: var(--muted);
}

/* bare — h. The countdown hero has nothing to reduce once the countdown is
   dropped, so this is type on the theme surface. Also the honest treatment
   for any future hero whose identity is a widget rather than an image. */
[data-hero-slim="bare"] .tn-page-header {
  min-height: 200px;
  background-image: none;
  border-bottom: 1px solid var(--rule);
}
/* 🚨 These three carry [data-banner="1"] to reach (0,3,0)+. PageHeader's own
   banner rules are `.tn-page-header[data-banner="1"] .tn-page-header__x`, i.e.
   (0,3,0) — an unqualified `[data-hero-slim="bare"] .tn-page-header__x` is
   only (0,2,0) and LOSES, so a page with a banner kept the black gradient and
   the white title while a page without one went bare. The mosaic block above
   qualifies for exactly this reason; this one did not, and the two treatments
   silently disagreed. */
[data-hero-slim="bare"] .tn-page-header__veil,
[data-hero-slim="bare"] .tn-page-header[data-banner="1"] .tn-page-header__veil {
  background: var(--bg);
}
[data-hero-slim="bare"] .tn-page-header__content {
  padding: 168px 24px 24px;
}
[data-hero-slim="bare"] .tn-page-header__title,
[data-hero-slim="bare"] .tn-page-header[data-banner="1"] .tn-page-header__title {
  color: var(--text);
  text-shadow: none;
}
[data-hero-slim="bare"] .tn-page-header[data-banner="1"] .tn-page-header__sub {
  color: var(--muted);
}

/* Phones. PageHeader's own @media rule is (0,1,0) and a media query adds no
   specificity, so without these the themed desktop values would also apply on
   a phone — the treatments have to restate their own mobile geometry. */
@media (max-width: 768px) {
  [data-hero-slim] .tn-page-header {
    min-height: 190px;
  }
  [data-hero-slim] .tn-page-header__content {
    padding-top: 92px;
    padding-bottom: 22px;
  }
  [data-hero-slim="mark"] .tn-page-header__content {
    padding-top: 100px;
  }
  [data-hero-slim="editorial"] .tn-page-header__content {
    padding-left: 20px;
  }
  [data-hero-slim="bare"] .tn-page-header {
    min-height: 160px;
  }
  [data-hero-slim="bare"] .tn-page-header__content {
    padding-top: 92px;
  }
}


/* ==========================================================================
   THE REEL / ARTIST TEASER  —  ported from dev 2026-09-09
   ==========================================================================
   Extracted RULE BY RULE from dev's interior.css rather than promoted as a
   whole file, deliberately: dev's copy is 38 KB ahead of this one and 644 of
   its added lines belong to three other sessions' unpromoted interior work
   (gallery tokens, the About page, /locations). Taking the file would have
   shipped all of that to every themed tenant at once.
   Contract: docs/REEL-API.md. Guard: scripts/check-teaser-frame.tsx.
   ========================================================================== */
/* The teaser.
   🚨 Scrims and text over PHOTOGRAPHS are theme-agnostic on purpose: a black
   scrim with white type reads on any picture under any theme, whereas
   var(--bg)/var(--text) would put near-black type on a black scrim on every
   dark theme. These rgba/white literals are acknowledged in
   css-color-baseline.json for exactly that reason — they are not brand
   colours and must not become tokens. */
.ti-teaser {
  position: relative; overflow: hidden;
  background: #000000; color: #ffffff;
  border-radius: var(--ti-video-radius, 8px);
  /* Lifts the reel off the page instead of butting a black slab against the
     surface behind it (operator, 2026-09-08: *"how about some drop shadow
     action on cards/buttons/slideshow?"*). Carries most on the light interior
     themes, where a 16:9 black band otherwise reads as a hole in the page;
     on a dark theme it is close to invisible, which is the correct amount.
     A knob, so a theme that wants none can set it to `none`. */
  box-shadow: var(--ti-teaser-shadow, 0 24px 60px rgba(0, 0, 0, 0.28));
}
/* 🚨 ONE SHAPE PER SLOT, and it is the preset's, not the viewport's.
   The reel player next door (.ti-video__frame) was moved to a 177.78% / 56.25%
   ratio pair on 2026-09-07 so "the slot is one shape whichever fills it" — but
   only the REEL half was changed. The teaser, which fills that very same slot
   when a studio has no footage, was still 56.25% with a 125% phone override:
   16:9 on desktop, 4:5 on a phone, and never the 9:16 the design called for on
   any viewport. So the mismatch that fix set out to remove still existed, in
   the other direction. Both halves now use the same two ratios and the same
   420px portrait cap.
   The frame comes from the preset's orientation (data-orientation on the root),
   NOT from a media query — a 9:16 reel is 9:16 on a desktop too; it just gets
   capped and centred rather than reshaped.
   padding-top rather than aspect-ratio because browserslist pins safari 12 for
   the kiosk iPads. */
.ti-teaser__stage { position: relative; width: 100%; padding-top: 177.78%; overflow: hidden; }
.ti-teaser[data-orientation="landscape"] .ti-teaser__stage { padding-top: 56.25%; }
/* The cap and centring go on the ROOT, because the pinned CTA, the Pause and
   the progress ticks are positioned against it — capping the stage alone would
   leave the controls hanging outside the visible frame. */
.ti-teaser[data-orientation="portrait"] {
  max-width: var(--ti-teaser-max, 420px);
  margin-left: auto;
  margin-right: auto;
}
.ti-teaser__slide {
  position: absolute; inset: 0; margin: 0; overflow: hidden;
  opacity: 0;
  transition: opacity var(--ti-fade, 200ms) linear;
}
.ti-teaser__slide[data-on="yes"] { opacity: 1; z-index: 2; }
.ti-teaser__img {
  display: block; width: 100%; height: 100%; object-fit: cover;
  /* FILL, never letterbox or distort — object-fit preserves the source's own
     aspect either way, so a fixed-ratio frame crops rather than stretches.
     --kb-pos centres the crop on the piece's focal point when the reel lab has
     set one; with none it falls back to centre, which is what every slide did
     before focal points existed. */
  object-position: var(--kb-pos, center);
  transform: scale(var(--ti-kb-from, 1.02));
  will-change: transform;
}
/* The Ken Burns: one animation per cut, whose DURATION is the cut length.
   The scheduler shortens each cut, so the zoom itself gets faster — that is
   what makes it read as acceleration rather than as a slideshow that skips. */
.ti-teaser__slide[data-on="yes"] .ti-teaser__img {
  animation: ti-kb var(--kb-dur, 2000ms) linear forwards;
}
@keyframes ti-kb {
  from { transform: scale(var(--ti-kb-from, 1.02)) translate(0, 0); }
  to   { transform: scale(var(--ti-kb-to, 1.18)) translate(var(--kb-x, -6%), var(--kb-y, 0)); }
}
/* 🚨 The bottom band of the stage is masked, and that is a fix, not a
   flourish. The Ken Burns zooms 1.02 -> 1.18 and pans, so the bottom edge of
   the photograph travels across the cut — on a wide reel that reads as the
   FRAME shifting rather than the picture moving, which is the one thing a
   fixed-ratio stage must never look like it is doing (operator, 2026-09-08:
   *"the bottom 20% (or so) of the slideshow should be a black mask so the
   zooming images bottom doesnt shift"*). The band also gives the lower-third
   type its ground, which is why it must sit ABOVE the image and BELOW the
   text.
   🚨 The z-indexes here are EXPLICIT and measured, not decorative. DOM order
   does not settle this: .ti-teaser__img carries `transform: scale()`, which
   paints it in the positioned-descendants step as though it were z-index 0 —
   so a plain ::before, positioned but with z-index auto and EARLIER in the
   DOM, paints underneath it. Probed in Chromium against a solid white slide:
   the band came back #ffffff, i.e. the mask was there and invisible. The
   three layers are therefore stated: image 0 (implicit, via its transform),
   mask 1, text 2. The slide is itself `position: absolute; z-index: 2`, so
   it is a stacking context and none of these escape to fight the end card
   (3), the pause button (4) or the pinned CTA (5).
   Colour and height are knobs. The default is the component's own black — it
   already sets `background: #000` with white type for the reason recorded
   above, so brand colours are opt-in per theme rather than assumed. */
.ti-teaser__slide::before {
  content: '';
  position: absolute;
  left: 0; right: 0; bottom: 0;
  height: var(--ti-teaser-mask-h, 22%);
  z-index: 1;
  pointer-events: none;
  background: var(
    --ti-teaser-mask,
    linear-gradient(to top, #000000 0%, rgba(0, 0, 0, 0.72) 42%, rgba(0, 0, 0, 0) 100%)
  );
}
.ti-teaser__text { position: absolute; inset: 0; margin: 0; pointer-events: none; z-index: 2; }
/* 🚨 TOP-left, because the bottom-left corner has THREE tenants and the caption
   is the one that does not need to be there.
   History, because two wrong answers were shipped before this one. The caption
   sat at `bottom: 8%`; a pass on 2026-09-08 moved it to `bottom: 34px` to clear
   the progress row, reading `bottom: 16px` at .ti-teaser__progress and missing
   that `.ti-teaser[data-state] .ti-teaser__progress` overrides it to 52px 140
   lines further down - every teaser carries data-state, so the ticks were never
   at 16px. 34px put the caption INSIDE .ti-teaser__cta (bottom 12px, opaque,
   z-index 5 against the caption's 2), so it went behind a button.
   Measured in Chromium with a CTA present, at 390/900/1400px in both
   orientations: `bottom: 34px` hits the CTA 6 times out of 6; the ORIGINAL
   `bottom: 8%` hits the CTA 4 times and the ticks 6 times, so reverting is not
   the fix either; `top: 14px` is clean in all six. The pinned CTA is on by
   default for every ReelEmbed and always on BoardPhone, so the collision is the
   common case, not the edge one. */
.ti-teaser__caption {
  position: absolute; left: 4%; top: 14px;
  font-family: var(--font-body); font-size: 12px; font-weight: 600;
  letter-spacing: 0.18em; text-transform: uppercase;
  color: #ffffff; opacity: 0.85; text-shadow: 0 1px 10px rgba(0, 0, 0, 0.55);
  animation: ti-cap var(--kb-dur, 2000ms) linear forwards;
}
/* The beats: lines of the artist's own info, big, one per cut, moving
   around the frame so the eye follows the edit. Entrance is a fraction of
   the cut (ti-cap's 12%), so on a 320 ms cut the line is fully on in ~40 ms
   — a hit, not a fade. */
.ti-teaser__beat {
  position: absolute; max-width: 78%;
  display: flex; flex-direction: column; gap: 6px;
  color: #ffffff; text-shadow: 0 2px 18px rgba(0, 0, 0, 0.6);
  animation: ti-cap var(--kb-dur, 2000ms) linear forwards;
}
.ti-teaser__beat[data-pos="bl"] { left: 4%; bottom: 16%; text-align: left; }
.ti-teaser__beat[data-pos="c"]  { left: 50%; top: 50%; text-align: center; align-items: center; max-width: 86%; animation-name: ti-cap-c; }
.ti-teaser__beat[data-pos="tr"] { right: 4%; top: 8%; text-align: right; align-items: flex-end; }
.ti-teaser__beat[data-pos="bc"] { left: 50%; bottom: 16%; text-align: center; align-items: center; animation-name: ti-cap-bc; }
/* The artist's own face, on the beat that says their name.
   Operator, 2026-09-08: "doesn't look like the artist photo is there/used".
   It WAS - on the end card, which on the shipped curve arrives about ten
   seconds in and holds for three, so almost nobody saw it. A reel that opens
   on a name is the one place a face belongs: it is the difference between a
   caption and a person. Rides the beat's existing entrance, so it costs no new
   animation, and follows the beat's own align-items so it sits correctly
   whichever corner the beat is cycled into. */
.ti-teaser__face {
  /* 🚨 A px fallback FIRST, and it is not decorative. browserslist pins
     safari 12 for the kiosk iPads and Safari 12 has no clamp(), so both
     declarations drop and an <img> with no width or height falls back to its
     INTRINSIC size - a full-size photograph in the middle of the frame. A
     font-size that falls back is a smaller word; this one is a broken layout,
     which is why this rule gets a fallback where the type in this file does
     not. */
  width: 56px; height: 56px;
  width: clamp(44px, 7vw, 72px); height: clamp(44px, 7vw, 72px);
  object-fit: cover;
  border-radius: var(--ti-chip-radius, 999px);
  border: 2px solid var(--accent);
}
.ti-teaser__kicker {
  font-family: var(--font-body); font-size: clamp(11px, 1.3vw, 14px); font-weight: 600;
  letter-spacing: 0.2em; text-transform: uppercase; opacity: 0.85;
}
.ti-teaser__line {
  font-family: var(--font-display); text-transform: uppercase;
  font-size: clamp(26px, 5.4vw, 68px); line-height: 0.95; letter-spacing: 0.01em;
  text-wrap: balance;
}
.ti-teaser__beat[data-kind="name"] .ti-teaser__line { font-size: clamp(34px, 8vw, 104px); }
.ti-teaser__beat[data-kind="count"] .ti-teaser__line { font-size: clamp(48px, 10vw, 128px); }
/* Sentences (tagline, bio) are prose, not a headline: body face, sentence
   case, a readable measure. */
.ti-teaser__beat[data-kind="tagline"] .ti-teaser__line,
.ti-teaser__beat[data-kind="bio"] .ti-teaser__line {
  font-family: var(--font-body); text-transform: none; font-weight: 500;
  font-size: clamp(17px, 2.4vw, 30px); line-height: 1.25; letter-spacing: 0; max-width: 32ch;
}
.ti-teaser__beat[data-kind="handle"] .ti-teaser__line { font-size: clamp(20px, 3.4vw, 44px); letter-spacing: 0.04em; }
/* "Books open" is the one beat that is news, so it gets the gold — as a
   SURFACE under the word, never as its colour (check-css-vocab R-8b). */
.ti-teaser__beat[data-kind="status"] .ti-teaser__line { border-bottom: 5px solid var(--accent); padding-bottom: 4px; }
@media (max-width: 719px) {
  .ti-teaser__beat { max-width: 88%; }
  .ti-teaser__beat[data-pos="tr"] { top: 6%; }
}
@keyframes ti-cap {
  from { opacity: 0; transform: translateY(6px); }
  12%  { opacity: 1; transform: translateY(0); }
  to   { opacity: 1; transform: translateY(0); }
}
@keyframes ti-cap-c {
  from { opacity: 0; transform: translate(-50%, calc(-50% + 6px)); }
  12%  { opacity: 1; transform: translate(-50%, -50%); }
  to   { opacity: 1; transform: translate(-50%, -50%); }
}
@keyframes ti-cap-bc {
  from { opacity: 0; transform: translate(-50%, 6px); }
  12%  { opacity: 1; transform: translate(-50%, 0); }
  to   { opacity: 1; transform: translate(-50%, 0); }
}
/* --- Kinetic text (preset textMotion: "kinetic") -------------------------
   The offer-ad studio's entrance vocabulary, re-homed. The KEYFRAMES port from
   components/marketing/TattooNOWVideoAd as they are — transform and opacity
   only, which every browser we ship to runs, and the same cubic-bezier(0.16,
   1, 0.3, 1) it uses. What did NOT port is that component's sizing: it scales
   type with container queries (container-type: size, cqw/cqh), which do not
   resolve on the kiosk iPads, so type here stays on the clamp()+vw scale the
   rest of this file uses.

   🚨 Under kinetic the CONTAINER stops animating and its children animate
   instead, so the entrance must restate the centring transform that ti-cap-c /
   ti-cap-bc were carrying — drop it and every centred beat jumps to the
   frame's left edge. Same reason the reduced-motion block below restates them.

   Entrances are a FRACTION of the cut, like ti-cap's 12%, so a 320 ms cut
   still lands the line instead of being caught mid-flight. */
.ti-teaser[data-motion="kinetic"] .ti-teaser__beat { animation: none; opacity: 1; }
.ti-teaser[data-motion="kinetic"] .ti-teaser__beat[data-pos="c"] { transform: translate(-50%, -50%); }
.ti-teaser[data-motion="kinetic"] .ti-teaser__beat[data-pos="bc"] { transform: translateX(-50%); }
.ti-teaser[data-motion="kinetic"] .ti-teaser__kicker {
  animation: ti-beat-left var(--kb-dur, 2000ms) cubic-bezier(0.16, 1, 0.3, 1) forwards;
}
.ti-teaser[data-motion="kinetic"] .ti-teaser__line {
  animation: ti-beat-right var(--kb-dur, 2000ms) cubic-bezier(0.16, 1, 0.3, 1) forwards;
}
/* A number and a books-open status are hits, not sentences: they punch in
   rather than slide, which is the offer ad's tnva-pop. */
.ti-teaser[data-motion="kinetic"] .ti-teaser__beat[data-kind="count"] .ti-teaser__line,
.ti-teaser[data-motion="kinetic"] .ti-teaser__beat[data-kind="status"] .ti-teaser__line {
  animation-name: ti-beat-pop;
}
@keyframes ti-beat-left {
  from { opacity: 0; transform: translateX(-12%); }
  14%  { opacity: 1; transform: translateX(0); }
  to   { opacity: 1; transform: translateX(0); }
}
@keyframes ti-beat-right {
  from { opacity: 0; transform: translateX(12%); }
  18%  { opacity: 1; transform: translateX(0); }
  to   { opacity: 1; transform: translateX(0); }
}
@keyframes ti-beat-pop {
  from { opacity: 0; transform: scale(0.86); }
  16%  { opacity: 1; transform: scale(1); }
  to   { opacity: 1; transform: scale(1); }
}
.ti-teaser__preload { position: absolute; width: 1px; height: 1px; opacity: 0; pointer-events: none; }
/* The end card — name, shop, styles, Book — over the last frame. */
.ti-teaser__card {
  position: absolute; inset: 0; z-index: 3;
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  gap: 10px; padding: 24px; text-align: center;
  background: rgba(0, 0, 0, 0.55);
  opacity: 0; pointer-events: none;
  transition: opacity 260ms ease-out;
}
.ti-teaser__card[data-on="yes"] { opacity: 1; pointer-events: auto; }
.ti-teaser__card-photo {
  width: 72px; height: 72px; object-fit: cover;
  border-radius: var(--ti-chip-radius, 999px); border: 2px solid var(--accent);
}
.ti-teaser__card-name {
  font-family: var(--font-display); font-size: clamp(28px, 6vw, 64px);
  line-height: 0.95; text-transform: uppercase; color: #ffffff;
}
.ti-teaser__card-meta {
  display: flex; flex-wrap: wrap; justify-content: center; gap: 6px 18px;
  font-size: 13px; letter-spacing: 0.12em; text-transform: uppercase; color: #ffffff; opacity: 0.85;
}
.ti-teaser__book { margin-top: 8px; }
/* A minimal card is the name and the ask, with room to breathe. The photo and
   meta rows are not hidden here — the component does not render them at all,
   so there is nothing to hide and no empty gap left behind.
   🚨 It keeps the SAME 0.55 scrim as the full card deliberately. A lighter
   scrim would let more of the last piece through, but white type over an
   arbitrary photograph is only reliably readable because that scrim is dark
   enough — which is the whole reason these are theme-agnostic black/white
   literals (see the note at the top of this block). The two cards differ in
   what they SAY, never in whether you can read them. */
.ti-teaser[data-endcard="minimal"] .ti-teaser__card { gap: 16px; }
/* Controls: a visible Pause (a loop without one is the thing the theme's
   motion rules forbid), and a tick per cut so the acceleration is legible. */
.ti-teaser__toggle {
  position: absolute; right: 12px; bottom: 12px; z-index: 4;
  padding: 7px 12px; font-family: var(--font-body); font-size: 12px; font-weight: 600;
  letter-spacing: 0.08em; text-transform: uppercase;
  color: #ffffff; background: rgba(0, 0, 0, 0.55); border: 1px solid rgba(255, 255, 255, 0.35);
  border-radius: var(--ti-chip-radius, 999px); cursor: pointer;
}
.ti-teaser__toggle:hover, .ti-teaser__toggle:focus-visible { background: rgba(0, 0, 0, 0.8); outline: 2px solid var(--accent); outline-offset: 2px; }
.ti-teaser__progress {
  position: absolute; left: 12px; right: 96px; bottom: 16px; z-index: 4;
  display: flex; gap: 3px;
}
.ti-teaser__tick { flex: 1; height: 3px; background: rgba(255, 255, 255, 0.3); }
.ti-teaser__tick[data-done="yes"] { background: var(--accent); }
.ti-teaser__tick[data-done="now"] { background: #ffffff; }
.ti-teaser[data-state="card"] .ti-teaser__progress { opacity: 0; }
@media (prefers-reduced-motion: reduce) {
  /* No motion until asked for: the component does not autoplay under this
     query, and if the visitor presses Play the cuts run but the zoom does not. */
  .ti-teaser__slide[data-on="yes"] .ti-teaser__img { animation: none; transform: scale(var(--ti-kb-from, 1.02)); }
  .ti-teaser__caption, .ti-teaser__beat { animation: none; opacity: 1; }
  .ti-teaser__beat[data-pos="c"] { transform: translate(-50%, -50%); }
  .ti-teaser__beat[data-pos="bc"] { transform: translateX(-50%); }
  /* The kinetic presets animate the kicker and the line themselves, so
     stopping the container is not enough to stop the motion. */
  .ti-teaser__kicker, .ti-teaser__line { animation: none; opacity: 1; transform: none; }
}
/* --- Video CTA + byline place (2026-09-07) -------------------------------
   The video is a way to book, so the ask is pinned for the whole run rather
   than living only on the end card, which is on screen ~3 s a loop. Sits
   opposite the Pause button; the black scrim reasoning from the teaser block
   applies here too — this floats over photographs. */
.ti-teaser__cta {
  position: absolute;
  left: 12px;
  bottom: 12px;
  z-index: 5;
  padding: 8px 14px;
  font-family: var(--font-body);
  font-size: 12px;
  font-weight: 700;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  text-decoration: none;
  color: var(--accent-text);
  background: #ffffff;
  border-radius: var(--ti-chip-radius, 999px);
}
.ti-teaser__cta:hover,
.ti-teaser__cta:focus-visible {
  background: var(--accent);
  color: var(--text);
  outline: 2px solid #ffffff;
  outline-offset: 2px;
}
/* The teaser's own progress ticks stop short of it. */
.ti-teaser .ti-teaser__cta ~ .ti-teaser__progress,
.ti-teaser__progress { left: 12px; right: 96px; }
.ti-teaser[data-state] .ti-teaser__progress { bottom: 52px; }
/* --- Mobile tap targets (2026-09-07 audit) -------------------------------
   Measured on a real 390x844 iPhone viewport across twelve pages. Horizontal
   overflow was clean everywhere; the failures were all TARGET SIZE — WCAG
   2.5.8 asks 24x24 CSS px, Apple 44pt, Google 48dp, and these sat at 12-21px.

   Every fix grows the TARGET, never the type: the small type in a utility
   strip or a breadcrumb is a deliberate typographic choice, and enlarging it
   would be answering a different question. Negative margins keep the
   surrounding layout the height it already was. */
@media (max-width: 768px) {
  .ti-teaser__cta {
    min-height: 40px;
    display: inline-flex;
    align-items: center;
  }
}
