/* Shared styles used across all themes (basket, dropdown/toggle, etc.).
   Theme-specific look and feel belongs in /Theme/{ThemeName}/css instead. */

/* Backstop for AI custom CSS that sets top/left/transform on the header without position.
   On a static element top/left are ignored while transform still applies, so
   `left:50%; transform:translateX(-50%)` renders the nav at x ≈ −600. `relative` makes
   those offsets meaningful without pulling the header out of flow the way `fixed` would
   for every existing site. AI sheets that set their own position (sticky/fixed) still win
   when they load later at equal specificity. */
.site-header {
    position: relative;
}

/* Backstop for AI custom CSS that sets `display` on the mobile menu panel's own class.
   The panel ships closed via Tailwind's `.hidden{display:none}`, but a generated sheet that
   writes e.g. `.bbq-panel{display:flex}` beats it — same specificity (0,1,0) and site.css
   loads after tailwind.build.css. Measured live on website-S1: the panel stood permanently
   open over the page on both generated sites. Attribute+class is (0,2,0), so a closed panel
   stays closed regardless of load order.
   ⚠️ `#mobile-menu` is the load-bearing half: conversion STRIPS `data-slot` from the stored
   headerHtml (verified in the browser — the served panel has the id but no data-slot), so the
   attribute selector alone matched nothing. The id is the same hook the toggle script uses via
   `data-toggle-target="mobile-menu"`. The attribute selector is kept for any markup that does
   retain it. The toggle still works: it removes `.hidden`, and
   with the class gone this rule no longer matches. */
#mobile-menu.hidden,
[data-slot="menu-panel"].hidden {
    display: none;
}

/* The site header is a TOP bar, never a side column. Owner's rule, 2026-08-29: "Menu nav should be
   always standart menu (top page). We need block other types of menu in the future."

   Measured on a generated site (cmp-pro-4): the model wrote
   `.site-header { width: var(--sidebar-width); height: 100vh; position: sticky }`, expecting a flex
   ROW wrapper around the header and the content. No such wrapper survives conversion — page assembly
   puts header/main/footer directly on body — so the header became a full-height block on the left and
   `main` flowed BELOW it. The result was a dark sidebar beside an empty white page.

   Only the side-column geometry is neutralised. `position: sticky` is left alone: a sticky top bar is
   a good design and a common one. `body > header.site-header` is (0,1,2) — it outranks the
   `.site-header` (0,1,0) a generated sheet can write, so load order does not matter. */
body > header.site-header {
    width: auto;
    height: auto;
    float: none;
}

/* ...and the menu inside it runs ACROSS, never down. Owner's rule, 2026-08-29: "Menu should be
   always in header (apart from mobile menu)."

   Fixing the header box alone was not enough — measured on cmp-pro-4 at 1440px, the header was
   full-width but 671px TALL, because the nav itself was `display:flex; flex-direction:column` and
   its four links sat at y=292, 367, 442 and 518. A vertical nav makes the header a wall whatever
   width it is.

   The second rule is the owner's stated exception and it carries an id, so it outranks the first
   whatever the order: the mobile panel's nav stays stacked. The panel is a DIV (`#mobile-menu`) and
   the desktop nav is nested inside the header's own wrapper (`.header-inner` on this site), which is
   why this is a descendant rule rather than a child one — models nest the nav differently every
   time, but never inside the panel. */
body > header.site-header nav {
    flex-direction: row;
    grid-auto-flow: column;
}

body > header.site-header #mobile-menu nav {
    flex-direction: column;
    grid-auto-flow: row;
}

/* TUB-14: a vertical menu the platform had to build from the DESKTOP link class — a header stored
   before the converter kept the panel's own link class, or a panel the design left without links.
   Desktop links are coloured for the header bar; measured live 2026-09-04 that put white links on
   the design's white panel and the menu vanished. The fallback nav paints itself in the site's own
   body pair, which is readable by construction, and stacks the links. Selector carries the panel id
   so it outranks a generated `#mobile-menu a { color }` (1,0,1) and `.nav-link` (0,1,0): this one is
   (1,2,4). Generated CSS cannot target `ttt-` classes at all. Sites regenerated after TUB-14 never
   carry the marker, so their own panel design is untouched. */
body > header.site-header #mobile-menu nav.ttt-menu-fallback {
    background-color: var(--site-bg, #ffffff);
    color: var(--site-ink, #1c1917);
    padding: 0.5rem 1rem;
    gap: 0;
}
body > header.site-header #mobile-menu nav.ttt-menu-fallback a {
    color: var(--site-ink, #1c1917);
    display: block;
    padding: 0.75rem 0;
}

[data-toggle-target].is-open .icon-open {
    display: none;
}

/* Toggled by Shared.Session when hydrating per-user header state onto the anonymous shell. */
.auth-hidden {
    display: none !important;
}

.basket-badge {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    min-width: 1.1rem;
    height: 1.1rem;
    padding: 0 0.25rem;
    border-radius: 9999px;
    font-size: 0.65rem;
    line-height: 1;
}

.basket-badge[data-basket-count="0"] {
    display: none;
}

/* Hero slider: horizontal scroll-snap carousel with the scrollbar hidden (Shared.Slider adds the
   visible prev/next/dot controls; native swipe/trackpad scroll works regardless). */
.hero-slider-track {
    -ms-overflow-style: none;
    scrollbar-width: none;
}

.hero-slider-track::-webkit-scrollbar {
    display: none;
}

.slider-dot.is-active {
    background-color: rgba(255, 255, 255, 0.95);
}

/* TUB-24 — the page shell's own structural layout (_PageShell.cshtml, main-sidebar skeleton).
   Measured live: a generated site's stylesheet can define `.flex { display:flex; align-items:center }`
   (or `.container`/`.grid`/`.inline`/`.center`), and /site.css loads after tailwind.build.css at equal
   specificity, so the design's rule beats Tailwind's bare `.flex`/`.flex-1`/`.w-full` on the shell's own
   row and the recipes column rendered vertically centred against a taller sidebar. `ttt-` is a reserved
   token CustomCssValidator already strips from every site rule's selector, so a class built from it is
   a name no design can ever reach. These are a straight rename of the Tailwind utilities that used to
   sit on this markup — the values are transcribed exactly, not redesigned. Shared between books and
   websites (both render through _PageShell); a book page's own stylesheet (book-print.css) never
   targets any of these tokens.

   `align-items: stretch` on the row is written explicitly even though it is flex's initial value: it
   is the declaration this ticket exists to protect, and an explicit one says so to the next reader. */
.ttt-shell-container {
    max-width: 80rem;             /* max-w-7xl */
    margin-inline: auto;          /* mx-auto */
    padding-inline: 1.5rem;       /* px-6 */
    padding-block: 3.5rem;        /* py-14 */
}

@media (min-width: 1024px) {
    .ttt-shell-container {
        padding-inline: 2rem;     /* lg:px-8 */
    }
}

/* Full-bleed band variant (a leading full-bleed component was hoisted above this container): no top
   padding, the band above already provides the visual breathing room. Higher specificity than
   .ttt-shell-container alone, so load order does not matter. */
.ttt-shell-container.ttt-shell-container-band {
    padding-top: 0;               /* pb-14 (no py-14) */
}

.ttt-shell-row {
    display: flex;
    flex-direction: column;       /* flex-col */
    align-items: stretch;         /* the invariant this ticket protects — see comment above */
    gap: 2.5rem;                  /* gap-10 */
}

@media (min-width: 1024px) {
    .ttt-shell-row {
        flex-direction: row;      /* lg:flex-row */
    }
}

.ttt-shell-main {
    flex: 1 1 0%;                 /* flex-1 */
    min-width: 0;                 /* min-w-0 */
}

.ttt-shell-aside {
    width: 100%;                  /* w-full */
    flex-shrink: 0;               /* flex-shrink-0 */
}

@media (min-width: 1024px) {
    .ttt-shell-aside {
        width: 18rem;              /* lg:w-72 */
    }
}

@media (min-width: 1280px) {
    .ttt-shell-aside {
        width: 20rem;              /* xl:w-80 */
    }
}

/* space-y-8 rhythm (Tailwind's `> * + * { margin-top }` shape, not `> * + *`, so a `[hidden]` widget
   never contributes a gap) — the sidebar's own children in the non-edit path, and the edit-mode
   sortable-root wrapper's children below, which is itself the sidebar's only child so needs its own
   copy of the rhythm. */
.ttt-shell-aside > :not([hidden]) ~ :not([hidden]),
.ttt-shell-stack > :not([hidden]) ~ :not([hidden]) {
    margin-top: 2rem;             /* space-y-8 */
}

/* Edit-mode component wrapper (PageComponentHtmlHelperExtensions.RenderComponentsAsync) and its
   "+" add-component placeholder. Plain CSS rather than Tailwind utility classes: this markup is
   built in a .cs file outside every Tailwind content glob in tailwind.config.js, so utility
   classes there would be purged from the compiled build (see the Provisioning/SeedApi globs,
   which exist for exactly this reason but only cover those two folders). */
.ttt-component-slot {
    position: relative !important;
    display: block !important;
    visibility: visible !important;
    opacity: 1 !important;
    pointer-events: auto !important;
}

.ttt-add-component {
    position: absolute !important;
    bottom: 0.75rem;
    left: 0.75rem;
    z-index: 30;
    display: flex !important;
    visibility: visible !important;
    opacity: 1 !important;
    pointer-events: auto !important;
    align-items: center;
    gap: 0.375rem;
}

/* Syncfusion's RichTextEditor gives its contenteditable area a default 16px padding on every side.
   Text component content is full-bleed HTML (sections already carry their own padding), so that
   default insets it ~16px narrower than the published page on both sides — the component visibly
   shrinks only while editing. Zero it out so the edit view matches the public render exactly. */
.e-richtexteditor .e-content {
    padding: 0 !important;
}

/* Syncfusion's inline RichTextEditor renders a static "preview" wrapper (.e-rte-content, and the
   control root itself while not focused) whose fluent theme paints it opaque white. That wrapper
   sits OUTSIDE whatever width/background the component's own content sets (e.g. a book recipe
   article narrower than its page canvas), so Syncfusion's white shows through as a band around the
   content while editing — a mismatch the published page (which has no such wrapper) never has.
   Same principle as the padding rule above: make the edit view match the public render. */
.e-richtexteditor.e-rte-inline,
.e-richtexteditor .e-rte-content {
    background-color: transparent !important;
}

/* "+" add-component button (Shared.AddComponent) rendered bottom-left of every component slot in
   edit mode. Hidden until the component is hovered (or the dropdown is open), so it never reads
   as part of the page's own content; the dropdown caret is hidden since "+" alone is the affordance.
   !important is needed on the Syncfusion-owned properties (background/border/color/caret) because
   the Syncfusion CDN theme stylesheet (_SyncfusionStyles) is linked after shared.css and wins
   equal-specificity ties on its own .e-btn/.e-caret rules.
   Border/icon use the same rust as the AI design panel header so the controls stay visible on
   white page backgrounds. */
.ttt-add-component-btn {
    position: static !important;
    display: inline-flex !important;
    visibility: visible !important;
    align-items: center;
    justify-content: center;
    width: 1.75rem;
    height: 1.75rem;
    border-radius: 9999px !important;
    background-color: transparent !important;
    color: rgb(var(--color-rust)) !important;
    font-size: 1.1rem;
    line-height: 1;
    border: 1px solid rgb(var(--color-rust)) !important;
    opacity: 0 !important;
    pointer-events: auto !important;
    transition: opacity 0.15s ease, background-color 0.15s ease;
}

.ttt-component-slot:hover .ttt-add-component-btn,
.ttt-add-component-btn.e-active {
    opacity: 1 !important;
}

.ttt-add-component-btn:hover {
    background-color: rgb(var(--color-rust) / 0.12) !important;
}

.ttt-add-component-btn .e-caret {
    display: none !important;
}

/* Kebab "⋮" component-options button (Shared.RemoveComponent), rendered to the right of the "+"
   inside the same .ttt-add-component host. Mirrors .ttt-add-component-btn's visual language
   (round, hover-revealed, rust border) so the pair reads as one control group. */
.ttt-component-menu-btn {
    position: static !important;
    display: inline-flex !important;
    visibility: visible !important;
    align-items: center;
    justify-content: center;
    width: 1.75rem;
    height: 1.75rem;
    border-radius: 9999px !important;
    background-color: transparent !important;
    color: rgb(var(--color-rust)) !important;
    font-size: 1rem;
    line-height: 1;
    border: 1px solid rgb(var(--color-rust)) !important;
    opacity: 0 !important;
    pointer-events: auto !important;
    transition: opacity 0.15s ease, background-color 0.15s ease;
}

.ttt-component-slot:hover .ttt-component-menu-btn,
.ttt-component-menu-btn.e-active {
    opacity: 1 !important;
}

.ttt-component-menu-btn:hover {
    background-color: rgb(var(--color-rust) / 0.12) !important;
}

.ttt-component-menu-btn .e-caret {
    display: none !important;
}

/* Drag handle (Shared.ComponentReorder), pinned to top-center of the component slot —
   deliberately separate from the "+"/"⋮" group at bottom-left so it reads as the slot's own
   control rather than part of the insert/remove menu. Centered (not a corner) so it never
   collides with the fixed bottom-left "Design with AI" panel, nor with the top-left component
   type badges some *Edit views render (see e.g. HeroEdit/Default.cshtml) — both corners are
   already spoken for. Mirrors the other buttons' visual language; a plain button (not a
   Syncfusion dropdown), so no .e-caret override is needed. */
.ttt-component-drag-btn {
    position: absolute !important;
    top: 0.75rem;
    left: 50%;
    transform: translateX(-50%);
    z-index: 30;
    display: inline-flex !important;
    visibility: visible !important;
    align-items: center;
    justify-content: center;
    width: 1.75rem;
    height: 1.75rem;
    border-radius: 9999px !important;
    background-color: transparent !important;
    color: rgb(var(--color-rust)) !important;
    font-size: 1rem;
    line-height: 1;
    border: 1px solid rgb(var(--color-rust)) !important;
    /* Intentionally opacity: 0 at rest, revealed on hover below — !important on BOTH so custom CSS
       can't pin this to a fixed value (plan: custom-css-opus5-vivid-vireo, chrome self-defence),
       without permanently disabling the hover reveal (which an !important on only one side would). */
    opacity: 0 !important;
    pointer-events: auto !important;
    cursor: grab;
    transition: opacity 0.15s ease, background-color 0.15s ease;
}

.ttt-component-slot:hover .ttt-component-drag-btn {
    opacity: 1 !important;
}

.ttt-component-drag-btn:hover {
    background-color: rgb(var(--color-rust) / 0.12) !important;
}

.ttt-component-drag-btn:active {
    cursor: grabbing;
}

/* A Layout's own handle is an exception to the centering above: centered over the whole
   multi-column block, it lands inside whichever column the midpoint happens to fall in (e.g. the
   boundary column of a two-equal split), reading as if it belongs to that column's content instead
   of the layout. Pin it to the outer top-right edge of the block instead, and mute it to grey (the
   same tone as the empty-column "+" affordance) so it reads as a distinct, lower-emphasis control
   from a child component's own rust handle sitting inside its column. */
.ttt-component-slot[data-component-type="Layout"] > .ttt-component-drag-btn {
    left: auto;
    right: 0.75rem;
    transform: none;
    color: rgba(28, 25, 23, 0.65) !important;
    border-color: rgba(28, 25, 23, 0.35) !important;
}

.ttt-component-slot[data-component-type="Layout"] > .ttt-component-drag-btn:hover {
    background-color: rgba(28, 25, 23, 0.12) !important;
}

/* Sortable's drag ghost/chosen classes (configured via ghostClass/chosenClass in
   Shared.ComponentReorder) so the dragged slot reads clearly while in flight. !important on the
   ghost opacity because the base .ttt-component-slot rule now pins opacity: 1 !important (see the
   chrome self-defence comment above it) — this state override needs the same weight to still win. */
.ttt-component-slot.ttt-sortable-ghost {
    opacity: 0.4 !important;
}

.ttt-component-slot.ttt-sortable-chosen {
    outline: 2px dashed rgb(var(--color-rust) / 0.5);
    outline-offset: 2px;
}

/* Layout columns (Layout/LayoutEdit Default.cshtml): a positioning context for the normal
   absolutely-positioned .ttt-add-component overlay when a column already has children. */
.ttt-layout-column {
    position: relative !important;
    display: block !important;
    visibility: visible !important;
    opacity: 1 !important;
    pointer-events: auto !important;
}

/* Empty layout column placeholder (LayoutEdit/Default.cshtml). Unlike the normal
   .ttt-add-component overlay (absolutely positioned over existing content), an empty column has
   nothing to overlay, so this variant lays out in flow instead: a dashed drop target with its own
   centered "+", always visible (not hover-gated like .ttt-add-component-btn normally is) since
   there's no surrounding content whose hover would reveal it. */
.ttt-add-component.ttt-add-empty-column {
    position: static !important;
    display: flex !important;
    visibility: visible !important;
    opacity: 1 !important;
    align-items: center;
    justify-content: center;
    min-height: 6rem;
    border: 2px dashed rgba(28, 25, 23, 0.25);
    border-radius: 0.75rem;
}

.ttt-add-empty-column .ttt-add-component-btn {
    opacity: 1 !important;
    background-color: rgba(28, 25, 23, 0.06) !important;
    border-color: rgba(28, 25, 23, 0.35) !important;
    color: rgba(28, 25, 23, 0.65) !important;
    mix-blend-mode: normal;
}

.ttt-add-empty-column .ttt-add-component-btn:hover {
    background-color: rgba(28, 25, 23, 0.12) !important;
}

/* Chrome self-defence (plan: custom-css-opus5-vivid-vireo / custom-css-reachable). Per-site custom
   CSS is validated to reject !important, so as long as the editor chrome's own visibility/position
   declarations here carry !important, no custom-CSS rule — however broad its selector (a bare `div`,
   `*`, or anything else that isn't itself blocked for naming the chrome directly) — can hide, detach
   or reposition these controls. pointer-events is also guarded because custom CSS can disable
   interaction without using !important at all (e.g. `div { pointer-events: none }`). Values below
   match each element's actual current layout exactly; this only makes them immune to override, it
   changes nothing visible today.

   ⚠️ This guard alone does NOT stop the editor-lockout hole (plan: editor-lockout) — z-index only
   orders siblings within the same stacking context, so custom CSS can paint an ordinary (no
   !important, no chrome selector) fixed full-screen layer in a stacking context that composites
   above this element regardless of these declarations. #ai-design-panel is additionally promoted
   into the browser's TOP LAYER via the Popover API (see the <script> at the end of
   _AiDesignPanel.cshtml) — top-layer content paints above all page content unconditionally, which is
   the actual fix for that hole. This guard and the top-layer promotion are complementary: top-layer
   stops the "paint an opaque layer above it" attack, this guard stops the (already pre-existing,
   unrelated) "target it directly with a broad selector" attack — neither alone is sufficient. */
#ai-design-panel {
    position: fixed !important;
    display: block !important;
    visibility: visible !important;
    opacity: 1 !important;
}

/* Scoped to the whole subtree, not just the panel element — see the pointer-events comment below for
   why an element-only guard leaves descendants exposed to a bare `div { opacity: 0 }`-style rule.
   display is deliberately NOT included here (unlike the element-only guard above): the panel's own
   hide/show toggles (the unsaved-changes banner, spinners, image previews, …) all use Tailwind's
   `.hidden` (display: none), and an !important subtree override on display would win over `.hidden`
   too, permanently un-hiding all of them. opacity/visibility carry no such conflict — nothing inside
   the panel legitimately sets either to hide itself. */
#ai-design-panel * {
    visibility: visible !important;
    opacity: 1 !important;
}

/* pointer-events must cover the whole panel SUBTREE, not just the panel element. It is an inherited
   property, but `div { pointer-events: none }` matches the panel's inner wrapper divs DIRECTLY, and a
   direct match beats an inherited value — so guarding only #ai-design-panel leaves the Save/Discard
   buttons inside those wrappers dead while the panel itself looks fine. Verified in a browser: with
   the element-only guard, the Save button computes `pointer-events: none`; with this one it computes
   `auto`. Scoped to chrome subtrees only — never .ttt-component-slot's children, which are the
   owner's own content and must keep whatever pointer behaviour their design asks for. */
#ai-design-panel,
#ai-design-panel * {
    pointer-events: auto !important;
}

/* Top-layer positioning (plan: editor-lockout). A `[popover]` element's UA stylesheet default is
   `inset: 0; margin: auto` (centers it, sized to content) — Tailwind's `bottom-4`/`left-2` utility
   classes on #ai-design-panel only override the `bottom`/`left` longhands individually, leaving the
   UA's `top: 0; right: 0` in place and over-constraining the box. Pin all four explicitly so the
   fixed bottom-left position is unambiguous once the element is shown in the top layer. Values
   match the Tailwind classes already on the element (bottom-4 = 1rem, left-2 = 0.5rem); this changes
   nothing visually, it only removes the ambiguity for browsers that support the Popover API. */
#ai-design-panel {
    top: auto !important;
    left: 0.5rem !important;
    bottom: 1rem !important;
    right: auto !important;
    margin: 0 !important;
}

/* #ttt-account-menu (TUB-30a — one avatar instead of four buttons) — same top-layer promotion, same
   reasoning, same self-defence shape as #ai-design-panel above. Replaces the old
   #ttt-owner-tools-menu (one popover behind the "Manage" pill alone): Manage/Platform/Sign out now
   collapse into ONE popover behind the avatar, with _OwnerToolsMenu/_PlatformAdminMenu nested inside
   it as sections rather than each opening their own dropdown. Positioned fixed near the top of the
   viewport rather than anchored under the avatar button: once promoted, the popover is no longer a
   descendant of the header's normal-flow box, so an `absolute right-0 mt-2` position has nothing to
   be relative to. A fixed corner is deliberately different from full CSS anchor positioning
   (`position-anchor`/`anchor()`), whose cross-browser support is newer and less certain — see the
   plan's GOTCHA 3 note to confirm rather than assume browser support. */
/* Positioning applies unconditionally (harmless while the popover is closed and not rendered at
   all). display/visibility/opacity are scoped to :popover-open ONLY — unlike #ai-design-panel, this
   element has a genuine closed state (native popover behaviour, equivalent to the <details> it
   replaced), and an unconditional `display: block !important` here would force it permanently open,
   breaking the toggle entirely. */
#ttt-account-menu {
    position: fixed !important;
    top: 5rem !important;
    right: 1rem !important;
    bottom: auto !important;
    left: auto !important;
    margin: 0 !important;
    z-index: 50;
}

#ttt-account-menu:popover-open {
    display: block !important;
    visibility: visible !important;
    opacity: 1 !important;
}

#ttt-account-menu:popover-open * {
    visibility: visible !important;
    opacity: 1 !important;
}

#ttt-account-menu,
#ttt-account-menu * {
    pointer-events: auto !important;
}

/* TUB-31 — reaches the TUB-30a header layout on every design that was already generated. An
   AI-authored header replaces the theme partials entirely and renders only {{slot:menu}}{{slot:account}}
   — the platform fills those in, but WHERE they sit is that design's own header markup (flex, or
   grid with pinned columns), made at generation time and never touched again by this ticket
   (GOTCHA 1: no stored HeaderHtml is edited, no migration, no Firestore write). SlottedHtmlRenderer
   wraps each slot's RENDERED OUTPUT in these reserved containers (HeaderViewComponent only — see its
   own comment); placing them here reaches every existing site/template with no data change.
   "ttt-" is a name CustomCssValidator forbids a design's own selectors from using at all, and
   TUB-23's @scope donut excludes .ttt-chrome subtrees from a site's rules matching (not from custom-
   property INHERITANCE — the avatar below relies on exactly that distinction) — so nothing a design
   generates can override the rules below, on the header's flex row or (GOTCHA 2) a grid one. */
.ttt-header-nav {
    /* TUB-31, reviewer: the card asks for the nav "aligned to the left, like logo with NICE MARGIN to
       logo". Without this the two touch on a design whose header container has no gap of its own —
       measured on the owner's "Red Pizza" site, which rendered "Red PizzaHOME". A margin here rather
       than a gap on the container, because the container belongs to the design and we do not style it. */
    margin-left: 2rem;
    margin-right: auto;
    grid-column: 1;
}

.ttt-header-account {
    margin-left: auto;
    justify-self: end;
    grid-column: -1;
}

/* TUB-31 §3 — the avatar's own colours, read from the design's palette at RENDER TIME (never
   stored — AvatarPaletteColors.Choose runs once per Account-slot render, not per element) with a
   MANDATORY 4.5:1 fallback to plain black/white baked in server-side. The four custom properties are
   set once, inline, on the [data-auth-widget] chrome root by Default.cshtml; ttt-chrome's donut does
   not block custom-property INHERITANCE (only rule matching), so a value set there reaches these
   classes normally. Literal fallback values here are the OLD stone-100/-400/-200/-600 fixed colours,
   used only if the inline style is ever absent (defensive; Default.cshtml always sets it). */
.ttt-avatar {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 2.5rem;
    height: 2.5rem;
    border-radius: 9999px;
    transition: opacity 0.15s ease, background-color 0.15s ease, color 0.15s ease;
}

.ttt-avatar:hover {
    opacity: 0.85;
}

.ttt-avatar--out {
    background-color: var(--ttt-avatar-circle-out, #f5f5f4);
    color: var(--ttt-avatar-fg-out, #a8a29e);
}

.ttt-avatar--in {
    background-color: var(--ttt-avatar-circle-in, #e7e5e4);
    color: var(--ttt-avatar-fg-in, #57534e);
}

/* TUB-30a §3.2 (card point 7) — the menu must be dark-on-white with visible borders and must never
   inherit a generated site's design. ttt-chrome (TUB-23's @scope donut) is one independent
   guarantee — a site's own custom CSS cannot cascade into a .ttt-chrome subtree at all; this is the
   SECOND, independent one: every colour/border below is a literal value under a "ttt-" name, never a
   bare Tailwind utility class, so even a rule that somehow reused a common utility's name (TUB-23/24's
   `.block { padding: 5rem 0 }`) cannot restyle it. CustomCssValidator already rejects "ttt-" appearing
   anywhere in a site's own selectors (ReservedSelectorTokens), so this is enforced twice over. */
.ttt-account-menu-panel {
    width: 16rem;
    padding-block: 0.5rem;
    background-color: #ffffff;
    color: #1c1917;
    border: 1px solid #e7e5e4;
    border-radius: 0.75rem;
    box-shadow: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1);
}

.ttt-account-menu-username {
    display: block;
    padding: 0 1rem 0.5rem;
    margin-bottom: 0.25rem;
    font-size: 0.8125rem;
    color: #57534e;
    border-bottom: 1px solid #f5f5f4;
}

.ttt-account-menu-heading {
    padding: 0.5rem 1rem 0.25rem;
    font-size: 0.625rem;
    line-height: 1;
    text-transform: uppercase;
    letter-spacing: 0.05em;
    color: #a8a29e;
}

.ttt-account-menu-item {
    display: block;
    width: 100%;
    padding: 0.5rem 1rem;
    font-size: 0.875rem;
    text-align: left;
    background: none;
    border: 0;
    color: #44403c;
    text-decoration: none;
    cursor: pointer;
}

.ttt-account-menu-item:hover {
    background-color: #fffbeb;
    color: #c1440e;
}

.ttt-account-menu-divider {
    margin-top: 0.25rem;
    padding-top: 0.25rem;
    border-top: 1px solid #f5f5f4;
}

/* TUB-30a — the editing switch that replaces the "Editing: On/Off" text pill. Visual only; the
   click binding (Shared.EditModeSwitch.bindSwitchButtons) still reads/writes aria-pressed, unchanged. */
.ttt-edit-switch {
    display: inline-flex;
    align-items: center;
    gap: 0.5rem;
    padding: 0;
    background: none;
    border: 0;
    color: #44403c;
    font-size: 0.8125rem;
    font-weight: 600;
    cursor: pointer;
}

.ttt-edit-switch-track {
    position: relative;
    display: inline-block;
    flex-shrink: 0;
    width: 2.25rem;
    height: 1.25rem;
    border-radius: 9999px;
    background-color: #d6d3d1;
    transition: background-color 0.15s ease;
}

.ttt-edit-switch-thumb {
    position: absolute;
    top: 0.125rem;
    left: 0.125rem;
    width: 1rem;
    height: 1rem;
    border-radius: 9999px;
    background-color: #ffffff;
    box-shadow: 0 1px 2px rgb(0 0 0 / 0.2);
    transition: transform 0.15s ease;
}

.ttt-edit-switch[aria-pressed="true"] .ttt-edit-switch-track {
    background-color: #c1440e;
}

.ttt-edit-switch[aria-pressed="true"] .ttt-edit-switch-thumb {
    transform: translateX(1rem);
}

.ttt-edit-switch-label {
    white-space: nowrap;
}

/* The label hides under the switch's own control at very narrow widths so the toggle itself never
   contributes to header crowding; the mobile drawer's block variant always keeps its label (drawer
   width is never the constraint there). */
@media (max-width: 639px) {
    .ttt-edit-switch:not(.ttt-edit-switch--block) .ttt-edit-switch-label {
        display: none;
    }
}

/* The layout-picker and remove-component modals toggle via JS swapping the `hidden`/`flex` classes
   (Shared.LayoutPicker / Shared.RemoveComponent) — position/visibility/opacity never change between
   those states, so only `display` is guarded per-state; guarding it unconditionally would break the
   open/close toggle itself. */
[data-layout-picker-modal],
[data-remove-component-modal] {
    position: fixed !important;
    visibility: visible !important;
    opacity: 1 !important;
}

/* Subtree guard, same reasoning as #ai-design-panel above — the modals' confirm/cancel buttons sit
   inside wrapper divs, so an element-only pointer-events guard leaves them unclickable. */
[data-layout-picker-modal],
[data-layout-picker-modal] *,
[data-remove-component-modal],
[data-remove-component-modal] * {
    pointer-events: auto !important;
}

[data-layout-picker-modal].hidden,
[data-remove-component-modal].hidden {
    display: none !important;
}

[data-layout-picker-modal].flex,
[data-remove-component-modal].flex {
    display: flex !important;
}

/* Category MultiSelect (Shared.CategoryMultiSelect) inside create-recipe / recipes-admin modals.
   Popup is body-appended with zIndex 10000; keep the input chrome aligned with other form fields. */
.ttt-category-multiselect.e-multiselect,
.ttt-category-multiselect.e-input-group {
    border-radius: 0.5rem;
    border-color: #d6d3d1;
    font-size: 0.875rem;
}

.ttt-category-multiselect.e-input-focus,
.ttt-category-multiselect.e-input-group.e-input-focus:hover:not(.e-success):not(.e-warning):not(.e-error):not(.e-disabled) {
    border-color: rgb(var(--color-rust)) !important;
    box-shadow: none;
}

.e-popup.ttt-category-multiselect {
    z-index: 10000 !important;
}

/* Blog page components (Recipes/RecentPosts/WatchVideos/PopularTags/Search/Newsletter/Categories):
   bespoke classes from the CoNaObiad blog mockup, ported here since they live outside every
   Tailwind content glob (freeform markup pasted into static component views). Rust color tracks
   the site theme via the same CSS variable Tailwind's `rust` utility resolves to. */
.section-label {
    font-family: 'Inter', sans-serif;
    font-size: 0.7rem;
    font-weight: 700;
    letter-spacing: 0.18em;
    text-transform: uppercase;
    color: rgb(var(--color-rust));
}

.card-lift {
    transition: transform 0.3s ease, box-shadow 0.3s ease;
}

.card-lift:hover {
    transform: translateY(-4px);
    box-shadow: 0 16px 32px rgba(0, 0, 0, 0.10);
}

.tag-pill {
    transition: background 0.2s, color 0.2s;
}

.tag-pill:hover {
    background: rgb(var(--color-rust));
    color: white;
}

.sidebar-heading {
    font-family: 'Playfair Display', Georgia, serif;
    font-size: 1.1rem;
    font-weight: 700;
    color: #1c1917;
    padding-bottom: 0.75rem;
    border-bottom: 2px solid #fbbf24;
    margin-bottom: 1.25rem;
    display: inline-block;
}

.cat-row {
    display: flex;
    align-items: center;
    justify-content: space-between;
    padding: 0.55rem 0;
    border-bottom: 1px solid #f5f0e8;
}

.cat-row:last-child {
    border-bottom: none;
}

/* Anchor-scoped on purpose: rows render as <span> while CategoryLink.Url is still "#", and a hover
   colour change on a non-navigable row is the same false affordance as the dead link itself. */
.cat-row:hover a.cat-name {
    color: rgb(var(--color-rust));
}

.pagination-btn {
    display: flex;
    align-items: center;
    justify-content: center;
    width: 2.25rem;
    height: 2.25rem;
    border-radius: 50%;
    font-size: 0.875rem;
    font-weight: 600;
    border: 1.5px solid #e7e0d6;
    color: #57534e;
    transition: all 0.2s;
}

.pagination-btn:hover,
.pagination-btn.active {
    background: rgb(var(--color-rust));
    color: white;
    border-color: rgb(var(--color-rust));
}

/* Touch targets: the desktop sizes above are below the 44px iOS/Android guideline, which matters on
   the public site. Bumped only on coarse pointers, so desktop density is unchanged. */
@media (pointer: coarse) {
    .pagination-btn {
        width: 2.75rem;
        height: 2.75rem;
    }

    .cat-row {
        padding: 0.85rem 0;
    }
}

/* Platform-admin site-template gallery — colour/type swatches (no thumbnail images). */
.tmpl-card {
    display: flex;
    flex-direction: column;
    border: 1px solid #e7e5e4;
    border-radius: 1rem;
    background: #fff;
    overflow: hidden;
}

.tmpl-swatch {
    --tmpl-primary: #C1440E;
    --tmpl-bg: #FDF8F0;
    --tmpl-serif: 'Playfair Display', serif;
    --tmpl-sans: Inter, sans-serif;
}

/* TUB-32 — 16:9, and the header/nav must survive the crop.
   The captures are 1200x675 (TemplateThumbnailComposer.Width/Height, and tools/shot-template.mjs
   uses the same size), i.e. 16:9 already. `height: 5.5rem` squashed the card to roughly 3.5:1 and
   `object-fit: cover` then took the MIDDLE of the picture — which is exactly where the site's header
   is not. The owner's report was "I need see menu nav on thumbnails": the nav was being cropped off
   the top of every card.
   aspect-ratio matches the source, so a real screenshot is now shown whole. object-position: top is
   for the legacy thumbnails that are not 16:9 — a template with no blob storage falls back to the
   raw Theme.HeroImageUrl, which can be any shape — and keeps the top of those visible too. */
.tmpl-thumb {
    display: block;
    width: 100%;
    aspect-ratio: 16 / 9;
    object-fit: cover;
    object-position: top;
    border-radius: 1rem 1rem 0 0;
    background: #f5f5f4;
}

/* The no-thumbnail fallback tile. Same aspect ratio as .tmpl-thumb above, or a row mixing the two
   would have cards of two different heights. */
.tmpl-swatch-bg {
    display: flex;
    align-items: flex-end;
    gap: 0.75rem;
    aspect-ratio: 16 / 9;
    padding: 1rem 1.25rem;
    background: var(--tmpl-bg);
    border-bottom: 1px solid #e7e5e4;
}

.tmpl-swatch-serif {
    font-family: var(--tmpl-serif);
    font-size: 1.75rem;
    font-weight: 700;
    line-height: 1;
    color: var(--tmpl-primary);
}

.tmpl-swatch-sans {
    font-family: var(--tmpl-sans);
    font-size: 0.95rem;
    font-weight: 500;
    color: #44403c;
    opacity: 0.85;
}

.tmpl-swatch-bar {
    margin-left: auto;
    width: 2.5rem;
    height: 2.5rem;
    border-radius: 0.5rem;
    background: var(--tmpl-primary);
}

.tmpl-card-body {
    padding: 1rem 1.25rem 1.25rem;
}

.tmpl-status {
    flex-shrink: 0;
    font-size: 0.65rem;
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: 0.06em;
    padding: 0.2rem 0.5rem;
    border-radius: 9999px;
    background: #f5f5f4;
    color: #57534e;
}

.tmpl-status-draft { background: #f5f5f4; color: #78716c; }
.tmpl-status-available { background: #dcfce7; color: #166534; }
.tmpl-status-claimed { background: #ffedd5; color: #9a3412; }
.tmpl-status-retired { background: #e7e5e4; color: #44403c; }

.tmpl-meta {
    display: grid;
    grid-template-columns: repeat(3, minmax(0, 1fr));
    gap: 0.5rem;
    font-size: 0.75rem;
}

.tmpl-meta dt {
    color: #a8a29e;
    text-transform: uppercase;
    letter-spacing: 0.04em;
    font-weight: 600;
    font-size: 0.65rem;
}

.tmpl-meta dd {
    color: #44403c;
    margin: 0.1rem 0 0;
    word-break: break-all;
}

.tmpl-meta code {
    font-size: 0.7rem;
}

.tmpl-actions {
    display: flex;
    flex-wrap: wrap;
    gap: 0.5rem;
}

.tmpl-btn {
    font-size: 0.75rem;
    font-weight: 600;
    padding: 0.35rem 0.75rem;
    border-radius: 9999px;
    border: 1px solid #d6d3d1;
    background: #fff;
    color: #44403c;
    cursor: pointer;
}

.tmpl-btn:hover {
    border-color: #a8a29e;
}

/* "See me" is an <a>, not a <button> — keep it the same pill as its neighbours. */
a.tmpl-btn {
    display: inline-block;
    text-decoration: none;
    line-height: 1.4;
}

.tmpl-btn:disabled {
    opacity: 0.5;
    cursor: not-allowed;
}

.tmpl-btn-primary {
    background: rgb(var(--color-rust, 193 68 14));
    border-color: rgb(var(--color-rust, 193 68 14));
    color: #fff;
}

.tmpl-btn-primary:hover {
    filter: brightness(0.95);
}

.tmpl-btn-muted {
    color: #78716c;
}

.tmpl-btn-danger {
    color: #b91c1c;
    border-color: #fecaca;
}

.tmpl-btn-danger:hover {
    border-color: #f87171;
    background: #fef2f2;
}

.tmpl-upload-label {
    position: relative;
    display: inline-flex;
    align-items: center;
    cursor: pointer;
}

.tmpl-upload-input {
    position: absolute;
    inset: 0;
    opacity: 0;
    cursor: pointer;
}

.tmpl-busy-label {
    display: inline-flex;
    align-items: center;
    gap: 0.35rem;
}

.tmpl-spinner {
    display: inline-block;
    width: 0.85em;
    height: 0.85em;
    border: 2px solid currentColor;
    border-right-color: transparent;
    border-radius: 9999px;
    animation: tmpl-spin 0.7s linear infinite;
    vertical-align: -0.1em;
}

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

.tmpl-thumb-wrap {
    display: block;
}

/* The hero overlay is invisible in edit mode without this, and the cause is two rules deep.
   Syncfusion ships `.e-rte-content .e-content img:not(.e-resize) { z-index: 1000 }`, and its
   `.e-rte-image` class — added by the editor's JS to content images, not by any stylesheet —
   supplies `position: relative`. z-index is inert on a static element, so it is the COMBINATION
   that promotes the hero photo above `.hero-overlay` (which sits at z-index auto) and hides the
   owner's colour/opacity entirely. Measured 2026-08-07: visitor `static/auto`, editor
   `relative/1000`.
   Syncfusion is loaded in layer(ttt-vendor) (_SyncfusionStylesLayered), and shared.css is
   unlayered, so this wins without a specificity fight — but the layer alone did NOT save us,
   because a layered rule still applies when nothing opposes it, and the AI's generated CSS
   declares no z-index on the hero image.
   Neutralising the promotion is preferred over raising the overlay: Syncfusion's image resize
   handles also sit at z-index 1000, and out-stacking them covers the handles.
   Anchored on .hero-overlay because the AI rewrites .hero-section / .hero-bg-image /
   .hero-image-wrapper on every generation — .hero-overlay is the name the platform itself
   treats as a contract (findHeroRoots, shared.js). */
.e-rte-content .e-content :has(> .hero-overlay) img {
    z-index: auto;
}

/* ── #ttt-ai-assistview: beat Tailwind preflight (unlayered) vs fluent.css in layer(ttt-vendor).
   Preflight zeros input/textarea padding/border/radius; an unlayered rule at any specificity beats
   any layered vendor rule. Scope ONLY to this root — do not un-layer _SyncfusionStylesLayered.
   AssistView appendTo(#ttt-ai-assistview) adds .e-aiassistview ON the host itself, so selectors
   must match the host as well as any nested .e-aiassistview. */
#ttt-ai-assistview,
#ttt-ai-assistview.e-aiassistview {
    font-size: 13px;
    overflow: hidden;
}

#ttt-ai-assistview.e-aiassistview,
#ttt-ai-assistview .e-aiassistview {
    height: 100%;
    width: 100%;
    max-width: 100%;
    border: 1px solid #e7e5e4;
    border-radius: 0.75rem;
    background: #fafaf9;
    overflow: hidden;
    box-sizing: border-box;
    box-shadow: 0 10px 25px rgba(28, 25, 23, 0.12);
}

#ttt-ai-assistview.e-aiassistview,
#ttt-ai-assistview.e-aiassistview *,
#ttt-ai-assistview .e-aiassistview,
#ttt-ai-assistview .e-aiassistview * {
    max-width: 100%;
    box-sizing: border-box;
}

#ttt-ai-assistview .e-footer,
#ttt-ai-assistview .e-footer-wrapper,
#ttt-ai-assistview .e-assist-textarea,
#ttt-ai-assistview .e-input-group {
    max-width: 100%;
    width: 100%;
    box-sizing: border-box;
}

/* Restore control chrome that preflight strips. */
#ttt-ai-assistview button {
    font: inherit;
    background-color: transparent;
    background-image: none;
    color: inherit;
    cursor: pointer;
}

#ttt-ai-assistview textarea,
#ttt-ai-assistview input,
#ttt-ai-assistview .e-assist-textarea,
#ttt-ai-assistview [contenteditable="true"] {
    font: inherit;
    color: inherit;
    background-color: #fff;
    border: 1px solid #d6d3d1;
    border-radius: 0.5rem;
    padding: 0.45rem 0.65rem;
    margin: 0;
}

#ttt-ai-assistview svg {
    display: inline-block;
    vertical-align: middle;
    width: 1rem;
    height: 1rem;
}

#ttt-ai-assistview .e-assist-send-icon,
#ttt-ai-assistview .e-assistview-icon,
#ttt-ai-assistview .e-assist-attachment-icon,
#ttt-ai-assistview .e-icons.e-assist-send,
#ttt-ai-assistview .e-btn,
#ttt-ai-assistview .e-tbar-btn {
    display: inline-flex !important;
    align-items: center;
    justify-content: center;
    min-width: 2rem;
    min-height: 2rem;
    padding: 0.25rem;
}

#ttt-ai-assistview .e-assist-banner {
    padding: 1.25rem 1rem;
    text-align: center;
    color: #57534e;
}

#ttt-ai-assistview .e-assist-banner-title {
    margin: 0 0 0.35rem;
    font-size: 1rem;
    font-weight: 600;
    color: #1c1917;
}

#ttt-ai-assistview .e-assist-banner-body {
    margin: 0;
    font-size: 0.8rem;
    line-height: 1.4;
}

#ttt-ai-assistview .ttt-ai-chat-options {
    display: flex;
    flex-wrap: wrap;
    gap: 0.35rem;
    margin-top: 0.5rem;
}

#ttt-ai-assistview .ttt-ai-chat-options button,
#ttt-ai-assistview [data-ai-chat-option] {
    font-size: 11px;
    padding: 0.25rem 0.6rem;
    border-radius: 9999px;
    border: 1px solid #d6d3d1 !important;
    background: #f5f5f4 !important;
    color: #44403c !important;
    cursor: pointer;
    display: inline-block;
    margin: 0.15rem 0.15rem 0 0;
    white-space: normal;
    max-width: 100%;
    word-break: break-word;
}

#ttt-ai-assistview .ttt-ai-chat-options button:hover,
#ttt-ai-assistview [data-ai-chat-option]:hover {
    border-color: #c2410c !important;
    color: #c2410c !important;
    background: #fff !important;
}

#ttt-ai-assistview.ttt-ai-chat-busy [data-ai-chat-option],
#ttt-ai-assistview [data-ai-chat-option]:disabled {
    pointer-events: none !important;
    opacity: 0.55;
    cursor: default;
}

#ttt-ai-assistview .ttt-ai-chat-undo {
    margin-top: 0.4rem;
    font-size: 11px;
    text-decoration: underline;
    color: #78716c;
    background: none;
    border: none;
    padding: 0;
    cursor: pointer;
    display: inline-block;
}

#ttt-ai-assistview .ttt-ai-chat-undo:hover {
    color: #c2410c;
}

/* Part 5 — spacing only inside the AssistView thread (padding / gaps). No colour or layout redesign. */
#ttt-ai-assistview .e-view-container,
#ttt-ai-assistview .e-content,
#ttt-ai-assistview .e-output-container,
#ttt-ai-assistview .e-output {
    padding-left: 0.75rem;
    padding-right: 0.75rem;
}

#ttt-ai-assistview .e-prompt-item,
#ttt-ai-assistview .e-output-item,
#ttt-ai-assistview .e-output-container > div {
    margin-bottom: 0.75rem;
}

#ttt-ai-assistview .e-prompt-content,
#ttt-ai-assistview .e-output-content,
#ttt-ai-assistview .e-content-wrapper {
    padding: 0.35rem 0;
}

#ttt-ai-assistview .ttt-ai-chat-options {
    margin-top: 0.65rem;
    margin-bottom: 0.35rem;
    gap: 0.45rem;
}

#ttt-ai-assistview .e-response-toolbar,
#ttt-ai-assistview .e-assist-toolbar,
#ttt-ai-assistview .e-output-toolbar,
#ttt-ai-assistview [class*="response-toolbar"],
#ttt-ai-assistview [class*="ResponseToolbar"] {
    margin-top: 0.5rem;
    margin-bottom: 0.35rem;
    padding-top: 0.25rem;
}

#ttt-ai-assistview .ttt-ai-chat-undo {
    margin-top: 0.55rem;
    margin-bottom: 0.25rem;
}

/* NEWS-1 (TUB-1) — inline-editable newsletter heading/text in edit mode.

   The affordance has to be visible without moving anything: a contenteditable that reflows on
   focus would shift the whole sidebar widget under the cursor. Hence an outline (which does not
   take part in layout) and a padding-free tint, never a border or margin. The "ttt-" prefix is a
   reserved token CustomCssValidator already rejects in selectors, so AI- or owner-authored custom
   CSS cannot hide this chrome. */
.ttt-newsletter-editable {
    outline: 1px dashed rgb(168 162 158 / 0.7);
    outline-offset: 2px;
    border-radius: 2px;
    cursor: text;
}

.ttt-newsletter-editable:hover {
    outline-color: rgb(120 113 108 / 0.9);
}

.ttt-newsletter-editable:focus {
    outline: 2px solid rgb(var(--color-rust));
    outline-offset: 2px;
}

/* The gear sits inside the component box (top-right), never fixed to a viewport corner — PANEL-1
   owns bottom-left (unsaved-changes toast) and bottom-right (AI Assist launcher). */
.ttt-newsletter-gear {
    opacity: 0;
    transition: opacity 120ms ease-in-out;
}

[data-ttt-newsletter-edit]:hover .ttt-newsletter-gear,
.ttt-newsletter-gear:focus-visible,
.ttt-newsletter-gear[aria-expanded="true"] {
    opacity: 1;
}

/* TUB-24 — Featured Categories editor mark (FeaturedCategoriesEdit/Default.cshtml). Was a PERMANENT
   `ring-2 ring-dashed ring-rust/60` — visible on every site for as long as edit mode is on, and
   `ring-dashed` is not a Tailwind class at all, so it rendered as a solid ring that read as part of
   the design. Every other editor affordance in this file only appears on hover
   (.ttt-newsletter-gear above, .ttt-add-component-btn below) — this one now matches. `outline` rather
   than `ring`/`border`: an outline never takes part in layout, so a hovered module cannot shift the
   page by a pixel. */
.ttt-featured-categories-edit {
    outline: 2px dashed transparent;
    outline-offset: 2px;
    transition: outline-color 120ms ease-in-out;
}

.ttt-featured-categories-edit:hover {
    outline-color: rgb(var(--color-rust) / 0.6);
}

.ttt-featured-categories-label {
    opacity: 0;
    transition: opacity 120ms ease-in-out;
}

.ttt-featured-categories-edit:hover .ttt-featured-categories-label {
    opacity: 1;
}

/* TUB-12 — "back to top" (Pages/Shared/_ScrollToTop.cshtml). Semi-transparent, bottom right,
   appears once the viewer has scrolled past 400px. Every selector carries the ttt- prefix, which
   CustomCssValidator reserves, so no AI-authored sheet can restyle or hide it. Neutral dark
   translucent rather than a theme colour: it sits over arbitrary generated designs and must stay
   readable on any of them. */
.ttt-scroll-top {
    position: fixed;
    right: 1rem;
    bottom: 1rem;
    z-index: 45;                    /* above page content, below the assist launcher (50) and modals */
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 2.75rem;
    height: 2.75rem;
    padding: 0;
    border: 0;
    border-radius: 9999px;
    background-color: rgba(15, 23, 42, 0.55);
    color: #fff;
    cursor: pointer;
    opacity: 0;
    visibility: hidden;             /* also takes it out of the tab order and the a11y tree */
    transform: translateY(0.5rem);
    transition: opacity 0.2s ease, transform 0.2s ease, background-color 0.2s ease;
}

.ttt-scroll-top svg { width: 1.25rem; height: 1.25rem; fill: currentColor; }

.ttt-scroll-top:hover,
.ttt-scroll-top:focus-visible { background-color: rgba(15, 23, 42, 0.85); }

.ttt-scroll-top--visible { opacity: 1; visibility: visible; transform: translateY(0); }

/* The AI assist launcher pill owns bottom-4 right-2 (≈16–58px up from the bottom edge) wherever
   edit mode renders it — website edit mode and the book editors. Lift above it rather than tracking
   a flag that can drift out of sync with the markup. */
body:has(#ttt-ai-assist-launcher) .ttt-scroll-top { bottom: 5.25rem; }

@media (prefers-reduced-motion: reduce) {
    .ttt-scroll-top { transition: none; transform: none; }
    .ttt-scroll-top--visible { transform: none; }
}

/* Chrome is never part of a printed page — same rule book-print.css applies to its own overlays. */
@media print {
    .ttt-scroll-top { display: none !important; }
}

/* TUB-25 GOTCHA 5 — the AdSense module's own reserved-space sizing lives HERE, under ttt- names,
   never as inline styles or bare Tailwind utility classes on the component markup. Two reasons:
   (1) the "ttt-" prefix is a reserved token CustomCssValidator already rejects in selectors (see the
   newsletter comment above), so no AI- or owner-authored custom CSS can zero out the reserved space
   and cause layout shift the moment a real ad loads; (2) it is the one place both the real ad
   wrapper (AdSense/Default.cshtml) and the placeholder (_AdSensePlaceholder.cshtml) share their
   sizing from, so a real ad and its own placeholder always occupy the identical box. Heights match
   Google's own leaderboard/skyscraper units (plan §3 rule 4). */
.ttt-adsense-landscape {
    min-height: 90px;
}

@media (min-width: 1024px) {
    .ttt-adsense-landscape {
        min-height: 100px;
    }
}

.ttt-adsense-portrait {
    min-height: 600px;
    max-width: 300px;
    margin-left: auto;
    margin-right: auto;
}

.ttt-adsense {
    display: block;
    width: 100%;
}

.ttt-adsense-placeholder {
    box-sizing: border-box;
    display: flex;
    align-items: center;
    justify-content: center;
    width: 100%;
    padding: 0.75rem 1rem;
    border: 1px dashed rgb(168 162 158 / 0.6);
    border-radius: 0.5rem;
    background-color: rgb(250 250 249 / 0.6);
    color: rgb(120 113 108);
    font-size: 0.75rem;
    line-height: 1.4;
    text-align: center;
}

.ttt-adsense-setup-link {
    color: rgb(var(--color-rust));
    text-decoration: underline;
    text-underline-offset: 2px;
}

/* Same "hidden until the owner needs it" convention as .ttt-newsletter-gear above. */
.ttt-adsense-gear {
    opacity: 0;
    transition: opacity 120ms ease-in-out;
}

[data-ttt-adsense-edit]:hover .ttt-adsense-gear,
.ttt-adsense-gear:focus-visible,
.ttt-adsense-gear[aria-expanded="true"] {
    opacity: 1;
}

/* TUB-34 — public templates gallery. New block, ttt-namespaced per plan §3 A3: this component can
   render on a page whose own generated CSS loads after this file at equal specificity, and "ttt-" is
   the one prefix CustomCssValidator.ReservedSelectorTokens keeps out of a design's reach. Leaves the
   admin gallery's .tmpl-thumb / .tmpl-swatch-bg (TUB-32) completely untouched. */
.ttt-tmpl-gallery-heading {
    margin: 0 0 1.5rem;
    font-size: 1.5rem;
    font-weight: 700;
    line-height: 1.2;
}

.ttt-tmpl-gallery-empty {
    margin: 0;
    color: #78716c;
    font-size: 0.9rem;
}

.ttt-tmpl-gallery-grid {
    display: grid;
    grid-template-columns: repeat(1, minmax(0, 1fr));
    gap: 1.5rem;
}

@media (min-width: 640px) {
    .ttt-tmpl-gallery-grid {
        grid-template-columns: repeat(2, minmax(0, 1fr));
    }
}

@media (min-width: 1024px) {
    .ttt-tmpl-gallery-grid {
        grid-template-columns: repeat(3, minmax(0, 1fr));
    }
}

.ttt-tmpl-gallery-card {
    display: flex;
    flex-direction: column;
    overflow: hidden;
    border: 1px solid #e7e5e4;
    border-radius: 1rem;
    background: #fff;
}

.ttt-tmpl-card-thumb {
    /* Plan §3 A3 — 16:9 by aspect-ratio, not a fixed height: the captures are already 1200x675
       (TemplateThumbnailComposer.Width/Height), so the source is 16:9 already. */
    display: block;
    width: 100%;
    aspect-ratio: 16 / 9;
    object-fit: cover;
    background: #f5f5f4;
}

.ttt-tmpl-card-thumb-placeholder {
    display: flex;
    align-items: center;
    justify-content: center;
    color: #a8a29e;
    font-size: 0.7rem;
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: 0.06em;
}

.ttt-tmpl-card-thumb-placeholder::before {
    content: "No preview";
}

.ttt-tmpl-gallery-card-body {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 1rem;
    padding: 1.1rem 1.25rem;
}

.ttt-tmpl-card-name {
    margin: 0;
    overflow: hidden;
    color: #1c1917;
    font-size: 1rem;
    font-weight: 600;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.ttt-tmpl-card-cta {
    flex-shrink: 0;
    padding: 0.45rem 1rem;
    border: 1px solid #d6d3d1;
    border-radius: 9999px;
    color: #44403c;
    font-size: 0.8rem;
    font-weight: 700;
    text-decoration: none;
    white-space: nowrap;
}

.ttt-tmpl-card-cta:hover {
    border-color: #a8a29e;
    background: #fafaf9;
}

.ttt-tmpl-gallery-sentinel {
    height: 1px;
}
