/* ==========================================================================
   nown-tiles.css — S.I.T.E.S Tiles
   Core tile styles: nav-dock, tactile buttons, cards, hero, footer, and the
   shared tile scaffolding. All values come from nown-plate.css tokens.
   ========================================================================== */

/* --------------------------------------------------------------------------
   1. Tactile buttons  (.btn-tactile + color variants)
   Solid depth via a 4px bottom border; :active presses down.
   -------------------------------------------------------------------------- */
.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-2);
  padding: var(--space-3) var(--space-5);
  font-family: var(--font-sans);
  font-size: var(--text-base);
  font-weight: 600;
  text-decoration: none;
  border-radius: var(--radius-md);
  cursor: pointer;
  border: 1px solid transparent;
  transition: transform .1s ease, border-bottom-width .1s ease, box-shadow .1s ease, background-color .15s ease;
  color: var(--color-text);
  background: var(--color-surface-2);
}
.btn-tactile {
  border-bottom-width: 4px;
  /* A top-lit wash over the fill — the same "light from above" story the inset highlight tells,
     at a scale that is actually visible on a 40px button. */
  background-image: var(--gradient-tactile);
  box-shadow: var(--shadow-tactile);
}
.btn-tactile:active {
  transform: translateY(3px);
  border-bottom-width: 1px;
  box-shadow: var(--shadow-tactile-active);
}

/* The button is NAMED gold, and on a site where `accent` is blue it rendered blue —
   the name described the framework default rather than the button. It now reads a
   dedicated `highlight` role, which is what makes a warm complement possible on an
   otherwise monochrome page. The fallback keeps every existing site identical: where
   no highlight is defined, it is still the accent. */
.btn-gold-tactile {
  background-color: var(--color-highlight, var(--color-accent));
  border-color: var(--color-highlight-deep, var(--color-accent-deep));
  border-bottom-color: var(--color-highlight-deep-2, var(--color-accent-deep-2));
  color: var(--color-highlight-on, var(--color-on-accent));
}

/* A warm hairline under EVERY section heading.
   One rule rather than a list of tile classes, because a list drifts: the next section
   tile added would silently miss it. `.tile > h2[data-role]` is the shared shape — a
   section tile's heading is a direct child h2 — while card tiles use h3 inside their
   grid, so they are untouched. The catalog keeps its own rule because its heading sits
   inside a <header>.
   This is the cheapest thing that stops a single-hue page reading flat: the eye gets a
   complementary anchor at every section start, not just the one. */
.tile-catalog__head h2,
.tile > h2[data-role] { position: relative; padding-bottom: var(--space-2); }
.tile-catalog__head h2::after,
.tile > h2[data-role]::after {
  content: "";
  position: absolute;
  left: 0;
  bottom: 0;
  width: 2.5rem;
  height: 3px;
  border-radius: 999px;
  background: var(--color-highlight, var(--color-accent));
}
/* WHERE THE MARK AIMS — it follows the HEADING'S TEXT, and the text's alignment is decided by a
   ladder: the copy exception, then the tile, then the site. CSS cannot ask an element which way
   its text went, so this has to mirror that ladder — role by role, override by override.

   IT WAS WRONG IN THREE WAYS AT ONCE, and the gate found the third only after a screenshot found
   the other two:
     - a `[data-role="title"]` section heading is LEFT (the copy exception names it) while the mark
       was drawn at 50% — "What it is" sat at the left edge with its gold mark under the middle of
       the tile;
     - the catalog's `[data-role="heading"]` is NOT named by the exception, so its text is CENTRED
       by the tile while its mark was at the left edge — fifteen stray dashes on the gallery;
     - and `[data-role="tier"]`, also unnamed and therefore centred, survived the first attempt at
       a fix, because that attempt assumed every `.tile > h2[data-role]` was left. `tile-units.cjs`
       caught it on the next run, which is the point of asserting the pairing rather than the two
       halves separately.

   So: a heading whose role the exception does NOT name follows the TILE — which is why the centred
   form is the unqualified default here and the copy roles are the exception, mirroring the copy
   rule itself rather than inverting it. The exception list and the thing it exempts are one
   decision; this is the second place that has to be written from the same list. */
.tile-catalog__head h2::after,
.tile > h2[data-role]::after { left: 50%; transform: translateX(-50%); }

/* ...except the roles the copy exception names, whose text is left, so their mark goes left. */
.tile > h2[data-role="headline"]::after,
.tile > h2[data-role="title"]::after,
.tile > h2[data-role="body"]::after { left: 0; transform: none; }

/* Then the tile, then the site. Every branch carries BOTH groups, or an override moves the text
   and leaves the mark behind it. */
.tile.tile--left > h2[data-role]::after,
.tile.tile--left .tile-catalog__head h2::after,
[data-align="left"] .tile > h2[data-role]::after,
[data-align="left"] .tile .tile-catalog__head h2::after { left: 0; transform: none; }

.tile.tile--center > h2[data-role]::after,
.tile.tile--center .tile-catalog__head h2::after,
[data-align="center"] .tile > h2[data-role]::after,
[data-align="center"] .tile .tile-catalog__head h2::after { left: 50%; transform: translateX(-50%); }

.tile.tile--right > h2[data-role]::after,
.tile.tile--right .tile-catalog__head h2::after,
[data-align="right"] .tile > h2[data-role]::after,
[data-align="right"] .tile .tile-catalog__head h2::after { left: auto; right: 0; transform: none; }
.btn-primary-tactile {
  background-color: var(--color-primary);
  border-color: var(--color-primary-deep);
  border-bottom-color: var(--color-primary-deep);
  /* `on-primary`, not `on-accent`. These two are DARK fills and need light text, while the
     accent is a light brass and needs dark text — the one token that used to serve both cannot
     survive a light accent. */
  color: var(--color-on-primary);
}
.btn-plum-tactile {
  background-color: var(--color-plum);
  border-color: var(--color-plum-deep);
  border-bottom-color: var(--color-plum-deep);
  color: var(--color-on-primary);
}
.btn-ghost {
  background: transparent;
  border: 1px solid var(--color-primary);
  color: var(--color-primary);
}
.btn-ghost:hover { background: color-mix(in srgb, var(--color-primary) 8%, transparent); }

/* --------------------------------------------------------------------------
   2. Glassmorphism card  (.glass-card)
   -------------------------------------------------------------------------- */
.glass-card {
  background: var(--color-glass);
  backdrop-filter: blur(16px);
  -webkit-backdrop-filter: blur(16px);
  border: 1px solid var(--color-glass-border);
  border-radius: var(--radius-xl);
  box-shadow: var(--shadow-card);
  padding: var(--space-6);
  transition: transform .2s ease, box-shadow .2s ease;
}
.glass-card:hover { transform: translateY(-3px); box-shadow: var(--shadow-elevated); }
.glass-card:active { transform: translateY(1px); }

/* --------------------------------------------------------------------------
   3. Generic tile scaffolding
   A tile root is `.tile .tile-<name>`; parts use `data-role`.
   -------------------------------------------------------------------------- */
.tile {
  container-type: inline-size;   /* self-aware: children use container queries */
  /* A TILE'S TEXT MAY NOT ESCAPE ITS OWN BOX.
     `data-span` legitimately squeezes a tile on the narrow tier — grid.html demonstrates
     exactly that, and at a 320px viewport one of its tiles ends up 70px wide with an `h3`
     box of 20px whose text needs 70. The text did not wrap, the tile's content spilled, and
     because nothing between it and the viewport clips, the PAGE scrolled sideways: measured
     2px past the viewport at 305px on Windows, and 2px at the CI runner's 320px, where the
     wider fonts pushed the same spill past the edge. `mobile-width.cjs` caught it on the
     runner and not on the development machine, which is why the fix was found by measuring
     the mechanism (the grid's content overflow) rather than the symptom (a 2px scroll).
     `break-word`, not `anywhere`: this may only break a word that would otherwise overflow,
     so it cannot re-flow text that fits, and it does not change min-content sizing the way
     `anywhere` does. It has no effect inside `.tile-code pre`, whose `white-space: pre`
     already governs wrapping. */
  overflow-wrap: break-word;
}

/* --------------------------------------------------------------------------
   3b. The tile panel — every tile is a defined rectangular box

   The panel is what makes a tile a box rather than a pile of content on the
   background: padding so nothing touches the edge, a radius so corners read as
   intended, and a fill the author chooses. The grid's gap does the separating, so
   two tiles side by side can never collide.

   Declared HERE, once, so a new tile inherits it. The catalog grew three tiles with
   no box at all (social, map, search) and the ones that did have a box disagreed on
   padding — 8px against 32px — because each tile declared its own.

   Every tile gets the box by DEFAULT. `data-panel` now only varies the fill.

     (unset)  the tile unit: inset, boundary, radius, surface fill, soft shadow
     surface  the same thing, stated explicitly
     accent   a soft tint of the accent colour, for visual pop
     plate    the unit with no fill and no shadow — for a band that should not read
              as a card but must keep the same inset and boundary rhythm
     none     the opt-out: full width, no inset, no boundary, no fill

   WHY THE DEFAULT CHANGED. `data-panel` used to be the only way to get a box, and it
   was set on 5 of 91 entries in the framework's own content file. Everything else fell
   through to whatever padding its wrapper happened to set, and most wrappers set
   `padding-block` only — so horizontal padding was zero. Measured on the deployed site:
   201 tile instances across 14 types with no horizontal inset, 62 of which rendered
   content outside the page container's content box, including the footer on all eight
   pages and half of /tiles. A box you have to opt into is a box most tiles will not have.

   The attribute selector still matters: at (0,2,0) it outranks a tile's own
   `padding: var(--space-16) 0` at (0,1,0), so `none` can opt a band out without an
   !important and without editing the tile.
-------------------------------------------------------------------------- */
.tile {
  --tile-panel-pad: var(--space-6);
  --tile-panel-radius: var(--radius-lg);
  --tile-panel-fill: var(--color-surface);
  /* The boundary has to have contrast against BOTH the page background and a filled tile.
     It used to be `--color-surface-2`, which measures **1.03:1** against the page — a
     boundary that is not a boundary, and the reason the tile units read as unbounded in a
     screenshot even though every tile had a border. `--color-border` is held to >=3:1
     against each surface (WCAG's floor for a UI boundary), in both themes. */
  --tile-panel-edge: 1px solid var(--color-border);

  padding: var(--tile-panel-pad);
  border: var(--tile-panel-edge);
  border-radius: var(--tile-panel-radius);
  background: var(--tile-panel-fill);
  /* The same paper grain the plate carries, so a tile reads as card stock rather than as a flat
     panel. Anything that uses the `background` SHORTHAND resets this too — which is exactly what
     should happen for `plate`, `none` and the announcement: an unfilled box has no paper. */
  background-image: var(--grain);
  box-shadow: var(--shadow-card);
}

/* The unit, unfilled. Padding and boundary are kept so it still aligns to the same
   rhythm as the cards around it — only the fill and the shadow go. */
.tile[data-panel="plate"] {
  background: none;
  box-shadow: none;
}

/* Stated explicitly for a reader of the content file; identical to the default. */
.tile[data-panel="surface"] {
  --tile-panel-fill: var(--color-surface);
}

/* A tint, not a slab. Solid accent would fight whatever is inside it and would need
   its own foreground colour; a mix of the accent into the surface reads as pop and
   inherits dark mode for free. */
.tile[data-panel="accent"] {
  --tile-panel-fill: color-mix(in srgb, var(--color-accent) 12%, var(--color-surface));
}

/* Explicitly nothing: no fill, no padding, no boundary, no radius. The opt-out has to
   outrank the default above, hence the same attribute-selector weight. */
.tile[data-panel="none"] {
  background: none;
  padding: 0;
  border: 0;
  border-radius: 0;
  box-shadow: none;
}

/* --------------------------------------------------------------------------
   4. Navigation  (.nav-dock)

   The dock is chrome rather than a tile in the grid, so its SHAPE is chosen by
   variant rather than by a viewport breakpoint: the author picks a menu bar
   configuration and gets it. Three ship:
     (default)      the floating pill — the framework's signature shape
     --bar          full-width bar, links centred
     --minimal      brand and the theme toggle only; links are hidden

   The one thing a variant cannot decide is how the LINKS reflow when the header
   is narrow, and that is a container query on .site-header, not a viewport media
   query (rule 5). It also had to move: the previous version was an
   @media (min-width: 768px) that turned the dock into a pill on wide screens,
   which is a page-level LAYOUT media query and a rule-5 violation sitting in the
   tile stylesheet. Querying the header's own width says the same thing without
   assuming the chrome is always as wide as the window.
   -------------------------------------------------------------------------- */
.site-header { container-type: inline-size; }

.nav-dock {
  background: var(--color-dock);
  backdrop-filter: blur(20px);
  -webkit-backdrop-filter: blur(20px);
  /* A FULL BOUNDARY AND A GUTTER AT EVERY WIDTH, not only above 48rem. Below that the dock was a
     full-bleed bar with a bottom rule and nothing else — so the dock, which is a tile like any
     other, lost both its boundary and its gap from the screen edge that every other tile has.
     `tests/tile-units.cjs` found it the first time it ran, on six pages at 375px. The container
     query below still narrows it to a centred pill; this makes the narrow shape a tile too. */
  border: 1px solid var(--color-dock-border);
  border-radius: var(--radius-pill);
  margin: var(--space-2) var(--space-6);
  padding: var(--space-2) var(--space-2);
  display: grid;
  /* RULE 1, SAID OUT LOUD. The dock is a tile by `data-tile-id` but NOT by class — it wears
     `.nav-dock`, because `.tile`'s box (24px padding, radius-lg, the surface fill) is not the
     pill's — so `.tile { text-align: center }` never reached it and its label text computed
     `left`. It LOOKED centred only because each link hugs its own nowrap text inside a grid the
     `justify-items` below centres. `tests/tile-units.cjs` measures the property rather than the
     impression, which is how the difference surfaced. */
  text-align: center;
  /* Was repeat(5, 1fr) - but the dock holds SIX links plus the theme toggle, so the
     seventh item wrapped onto a second row and the bar changed height depending on how
     many links the content file happened to define. auto-fit sizes to the count.

     THE MINIMUM IS THE WHOLE FIX. It was 3.25rem (52px), which is narrower than the
     widest label: at 320px the grid made five 60.8px tracks, "Technical" needs 84px, and
     because justify-items centres it the label spilled BOTH ways past the track — 1px
     past the viewport and a horizontally scrolling page.

     A track narrower than its content cannot contain that content, so the minimum has to
     be wider than the widest label the dock can hold. 5.5rem (88px) clears "Technical"
     at 84px with room to spare. The cost is more rows on a very narrow screen (three at
     320px instead of two), which is the honest trade: a taller dock beats a page that
     slides sideways, and it needs no breakpoint to do it — auto-fit simply makes wider
     tracks and therefore fewer columns.

     Measured, not guessed: 320/340/360/375/390/414px and every page, before and after. */
  grid-template-columns: repeat(auto-fit, minmax(5.5rem, 1fr));
  justify-items: center;
  align-items: center;
  gap: 0;
}
/* Nav links live in a wrapper so they can be generated from content.json.
   `display: contents` removes that wrapper from the layout, so its children become
   direct grid/flex items of .nav-dock. Without it the dock collapses into a column. */
.nav-dock__links { display: contents; }

.nav-dock-link {
  display: flex;
  flex-direction: column;
  align-items: center;
  text-decoration: none;
  color: var(--color-text);
  opacity: .78;
  padding: var(--space-2) var(--space-3);
  border-radius: var(--radius-md);
  /* Was .55rem — 8.8px, which is below any sensible reading size and was the whole
     reason the bar read as "a little bit small". The link is the primary navigation;
     it should be legible at a glance, not a caption. */
  font-size: .8rem;
  font-weight: 600;
  letter-spacing: .01em;
  transition: all .2s cubic-bezier(.25,.8,.25,1);
  white-space: nowrap;
}
.nav-dock-link svg { width: 16px; height: 16px; margin-bottom: 2px; }
/* The active page used to be signalled by --color-accent alone, which on a site whose
   accent is a cool colour barely differs from the resting state. The highlight role
   gives it a warm mark, and the underline makes "you are here" unambiguous. */
.nav-dock-link:hover,
.nav-dock-link.active,
.nav-dock-link[aria-current="page"] {
  opacity: 1;
  color: var(--color-highlight, var(--color-accent));
  background: color-mix(in srgb, var(--color-highlight, var(--color-accent)) 12%, transparent);
  transform: translateY(-2px);
}
/* The theme toggle is a <button>, not an <a>, so without this it carries the UA's
   `buttonface` background and a 2px default border. In light mode that reads as a grey
   button with dark text and looks deliberate; in DARK mode it is a light-grey box with
   near-white text — measured 1.03:1, an invisible control. Reset the UA styling and keep
   the divider that was the point of the rule. */
.nav-dock-theme {
  background: none;
  border: 0;
  cursor: pointer;
  border-left: 1px solid color-mix(in srgb, var(--color-primary) 18%, transparent);
  padding-left: var(--space-3);
  margin-left: var(--space-2);
}

@container (min-width: 48rem) {
  .nav-dock {
    border: 1px solid var(--color-dock-border);
    border-radius: var(--radius-pill);
    display: flex;
    gap: var(--space-1);
    padding: var(--space-2) var(--space-4);
    max-width: fit-content;
    margin: var(--space-2) auto;
    box-shadow: var(--shadow-float);
  }
  .nav-dock-link { padding: var(--space-2) var(--space-4); font-size: .875rem; }
  .nav-dock-link svg { width: 18px; height: 18px; }
}

/* ---- variant: bar ---------------------------------------------------------
   Full width, square, pinned to the top of the header. Overrides the pill in
   both tiers, so it is a bar on a phone too — which is the point of choosing it. */
.nav-dock.tile-nav-dock--bar {
  border: 0;
  border-bottom: 1px solid var(--color-dock-border);
  border-radius: 0;
  max-width: none;
  margin: 0;
  box-shadow: none;
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: var(--space-1);
  padding: var(--space-3) var(--space-5);
}
.nav-dock.tile-nav-dock--bar .nav-dock-link {
  flex-direction: row;
  gap: var(--space-2);
  font-size: .8rem;
  padding: var(--space-2) var(--space-3);
}
.nav-dock.tile-nav-dock--bar .nav-dock-link svg { margin-bottom: 0; }

/* ---- variant: minimal -----------------------------------------------------
   Brand and the theme toggle only. Links stay in the markup — this is chrome,
   and a nav that removes its own destinations from the DOM would cost the crawl
   path — so they are hidden by clip rather than removed. */
.nav-dock.tile-nav-dock--minimal .nav-dock__links,
.nav-dock.tile-nav-dock--minimal .nav-dock-link:not(.nav-dock-theme) {
  position: absolute; width: 1px; height: 1px; overflow: hidden;
  clip-path: inset(50%); white-space: nowrap;
  padding: 0; margin: 0; border: 0;   /* a clipped 24px box is still a 24px box */
}
.nav-dock.tile-nav-dock--minimal {
  border-radius: 0; border: 0; max-width: none; margin: 0;
  box-shadow: none; display: flex; justify-content: flex-end;
  padding: var(--space-2) var(--space-4);
}
/* nav-dock surfaces are token-driven (--color-dock / --color-dock-border),
   so no component-specific dark overrides are needed. */

/* --------------------------------------------------------------------------
   5. Hero tile
   -------------------------------------------------------------------------- */
/* Vertical rhythm is this tile's own; the horizontal inset comes from the tile unit,
   so the padding is declared per axis rather than with a shorthand that would zero the
   sides. (It was `padding: var(--space-16) 0`, which is why the hero's heading sat
   flush against its own boundary once the unit gave it one.) */
.tile-hero { padding-block: var(--space-16); }
/* The tile root is the container; the INNER grid is what reflows. A container
   query always resolves against the nearest ANCESTOR container, so an element can
   never query itself — `.tile-hero` must therefore not be the element that
   changes. (This was a real bug: the hero silently stayed one column.) */
.tile-hero__grid { display: grid; gap: var(--space-8); align-items: center; }
.tile-hero__content h1 { font-size: clamp(2rem, 5vw, 3.5rem); margin: 0 0 var(--space-4); }
.tile-hero__content p { font-size: var(--text-lead); color: var(--color-text-muted); max-width: 60ch; }
.tile-hero__actions { display: flex; flex-wrap: wrap; gap: var(--space-3); margin-top: var(--space-6); }
/* Two columns only when the hero carries a visual, so there is no empty track. */
@container (min-width: 45rem) {
  /* Keyed on a real source, not on the wrapper: an unfilled media slot is hidden
     by the rule above, and a two-column grid with an invisible second column
     would leave the headline half-width with nothing beside it. */
  .tile-hero__grid:has(img[src]:not([src=""])) { grid-template-columns: 1.1fr .9fr; }
}

/* ---- centering / alignment helpers (usable on any tile) ---- */
/* --------------------------------------------------------------------------
   5b. Alignment — centred is the DEFAULT, and copy is the one exception

   Everything a tile shows is centred, chrome and footer included. The exception is the
   title and the body of copy: those two are what someone actually reads a paragraph of,
   and centred prose is harder to read than left-aligned prose. So the default is centre
   and the copy block opts back out.

   Three levels, and each one is set from the content file rather than by editing markup:

     site   `site.layout.align`  → `data-align` on the container ("center" | "left")
     tile   `entry.align`        → `.tile--left` | `.tile--center` | `.tile--right`
     block  the copy roles, which stay left unless a tile says otherwise

   Both the binder and the baker apply the site and tile levels, because they are
   hand-written mirrors of the same rules and `tests/binder-roundtrip.cjs` compares their
   DOMs. A rule added to one and not the other is a bug that shows up only in the BAKED
   html — that is, to a crawler and to anyone with JavaScript off — which is the one place
   nobody looks. Keep them in step.

   Specificity, deliberately: the tile level is written `.tile.tile--x` at (0,2,0) so it
   ties with the site level `[data-align] .tile` and wins on source order. Written as a
   bare `.tile--x` it would be (0,1,0) and a site-level default would silently beat a
   per-tile override, which is the opposite of the documented order.
-------------------------------------------------------------------------- */
.tile { text-align: center; }
/* Action rows are flex containers, so `text-align` does not move them. */
.tile .tile-hero__actions,
.tile [data-role="actions"] { justify-content: center; }
.tile .tile-hero__grid { justify-items: center; }
.tile .tile-hero__content,
.tile > [data-role="content"] { margin-inline: auto; }

/* THE COPY EXCEPTION. The roles a person reads as copy, at the unit's own inset — plus `pre` and
   `li`, which are copy by construction.

   `pre`: a code block and a composed message are both read line by line, and a centred one has no
   left edge to scan. Both `<pre>`s in the catalog — the code tile's `[data-role="language"]` and
   the contact form's composed message — were inheriting the tile's `center`, because neither
   carried a role this list named.

   `li`: a bullet is positioned `outside` — pinned to the list item's LEFT edge — while the text it
   belongs to follows `text-align`. Centre the item and the two come apart: measured on the price
   tile, the text centred at x=378 in a 450px box while the markers stayed at 153, leaving four dots
   stranded 85–120px from the words they mark. Nothing was "wrong" on either side of that; the
   marker and the text were simply obeying different rules, which is the same shape as the heading
   mark above. All three `<ul>`s in the catalog are lists of copy — a menu, the price features and
   visiting hours — so this is the whole list, not a sample of it.

   `tests/tile-units.cjs` measures the property, so the exception list has to BE the whole list. */
.tile [data-role="headline"],
.tile [data-role="title"],
.tile [data-role="body"],
.tile pre,
.tile li { text-align: left; }

/* The footer's two rows are flex containers carrying their own `justify-content`, so
   `text-align` cannot move them and they need saying explicitly. Without this the footer's
   brand and legal line centre while its link row stays hard left — half a footer. */
.tile .tile-footer__cols,
.tile .tile-footer__legal { justify-content: center; }
[data-align="left"] .tile .tile-footer__cols,
[data-align="left"] .tile .tile-footer__legal { justify-content: flex-start; }
.tile.tile--left .tile-footer__cols,
.tile.tile--left .tile-footer__legal { justify-content: flex-start; }
.tile.tile--right .tile-footer__cols,
.tile.tile--right .tile-footer__legal { justify-content: flex-end; }

/* Site level — a whole site can go back to left-aligned. */
[data-align="left"] .tile { text-align: left; }
[data-align="left"] .tile .tile-hero__actions,
[data-align="left"] .tile [data-role="actions"] { justify-content: flex-start; }
[data-align="left"] .tile .tile-hero__grid { justify-items: start; }

/* Tile level — one tile overrides the site. */
.tile.tile--left { text-align: left; }
.tile.tile--left .tile-hero__actions,
.tile.tile--left [data-role="actions"] { justify-content: flex-start; }
.tile.tile--left .tile-hero__grid { justify-items: start; }
.tile.tile--left [data-role="headline"],
.tile.tile--left [data-role="title"],
.tile.tile--left [data-role="body"] { text-align: left; }

.tile.tile--center { text-align: center; }
.tile.tile--center .tile-hero__actions,
.tile.tile--center [data-role="actions"] { justify-content: center; }
.tile.tile--center .tile-hero__grid { justify-items: center; }
.tile.tile--center [data-role="headline"],
.tile.tile--center [data-role="title"],
.tile.tile--center [data-role="body"] { text-align: center; }

.tile.tile--right { text-align: right; }
.tile.tile--right .tile-hero__actions,
.tile.tile--right [data-role="actions"] { justify-content: flex-end; }
.tile.tile--right .tile-hero__grid { justify-items: end; }
.tile.tile--right [data-role="headline"],
.tile.tile--right [data-role="title"],
.tile.tile--right [data-role="body"] { text-align: right; }

/* ---- hero variant: stacked ------------------------------------------------
   The logo treatment: the visual centres ABOVE the copy instead of beside it.
   Placed after the alignment block on purpose — the copy exception above sets
   [data-role="headline"]/[data-role="body"] left at (0,2,0), so these rules
   must come later to win their ties. */
/* Single column even when a visual is set (beats the :has two-column rule). */
.tile-hero--stacked .tile-hero__grid:has(img[src]:not([src=""])) { grid-template-columns: 1fr; }
.tile-hero--stacked .tile-hero__grid { justify-items: center; text-align: center; }
.tile-hero--stacked [data-role="headline"],
.tile-hero--stacked [data-role="title"],
.tile-hero--stacked [data-role="body"] { text-align: center; }
/* The figure follows the copy in the markup; pull it above and cap its size. */
.tile-hero--stacked .tile-hero__grid > figure { order: -1; max-width: 15rem; margin-inline: auto; }

/* Buttons that should sit on the baseline of their row, regardless of how much
   copy the neighbouring tiles carry. */
.tile-card > [data-role="actions"],
.tile-price > [data-role="actions"],
.tile-feature > [data-role="actions"],
.tile-team > [data-role="actions"] { margin-top: auto; }
.tile-price > .btn, .tile-product > .btn { margin-top: auto; }
/* Keep a row of tactile buttons optically level. */
.tile-hero__actions > .btn, [data-role="actions"] > .btn { align-self: center; }

/* --------------------------------------------------------------------------
   6. Card tiles (content-card, feature, team, testimonial, product, price)
   -------------------------------------------------------------------------- */
/* Layout only — the box is the tile unit's. These five types used to redeclare an
   identical padding/border/radius/fill/shadow here, which is how a second box system grew
   up beside the panel one and made "which rule decides a tile's box?" unanswerable. */
.tile-card, .tile-feature, .tile-team, .tile-testimonial, .tile-price {
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
}
/* A catalog group: a labelled band holding live tile examples. The head is what the
   label and note bind to; the body is the grid the examples sit in.

   The band keeps a generous vertical rhythm and takes its horizontal inset from the tile
   unit. It needs this declared because `.section` (nown-plate.css, loaded first) sets
   `padding-block` at the same specificity, so the unit's `padding` shorthand would win and
   flatten the band from 64px to 24px. */
.tile-catalog { padding-block: var(--space-16); }
.tile-catalog__head { margin-bottom: var(--space-5); }
.tile-catalog__head h2 { margin: 0 0 var(--space-2); font-size: 1.3rem; letter-spacing: -0.01em; }
.tile-catalog__head p { margin: 0; color: var(--color-text-muted); max-width: 68ch; }
.tile-catalog__body { padding-block: var(--space-1); }
.tile-catalog__tools { display: flex; flex-wrap: wrap; gap: var(--space-2); margin-top: var(--space-4); }

.tile-card__media, .tile-feature__media { aspect-ratio: 16 / 9; overflow: hidden; border-radius: var(--radius-md); background: var(--color-surface-2); }
.tile-card h3, .tile-feature h3, .tile-team h3, .tile-price h3 { margin: 0; }
.tile-card p, .tile-feature p, .tile-testimonial p { margin: 0; color: var(--color-text-muted); }
.tile-card__meta { font-weight: 600; color: var(--color-primary); }
/* The amount was grouped with the card meta line above, so a $500 headline rendered
   at caption size — the one thing on a pricing card that has to be readable at a
   glance. It gets the display face and real weight. */
.tile-price__amount {
  font-family: var(--font-display);
  font-size: 2.5rem;
  font-weight: 700;
  line-height: 1;
  letter-spacing: -0.02em;
  color: var(--color-text);
  margin: 0;
}

/* --------------------------------------------------------------------------
   7. Contact / form tiles
   -------------------------------------------------------------------------- */
/* The box comes from the tile unit. This rule used to add its own 32px inset and a 24px
   radius against the unit's 24px and 16px, which made the two form tiles the only ones on
   the site with a different corner radius. */
.tile-contact, .tile-contact-form {
  display: grid;
  gap: var(--space-4);
}
.tile-contact-form label { display: grid; gap: var(--space-1); font-weight: 600; font-size: .9rem; }
.tile-contact-form input, .tile-contact-form textarea {
  width: 100%;
  padding: var(--space-3);
  border: 1px solid var(--color-surface-2);
  border-radius: var(--radius-md);
  background: var(--color-background);
  color: var(--color-text);
  font: inherit;
}
.tile-contact-form input:focus, .tile-contact-form textarea:focus { outline: 2px solid var(--color-accent); outline-offset: 1px; }
/* Status feedback. Colour comes from tokens, never an inline style, so a theme change reaches
   it and dark mode stops being whatever the JS happened to hardcode. */
.tile-contact-form__status.is-ok { color: var(--color-success); }
.tile-contact-form__status.is-error { color: var(--color-danger); }
/* The endpoint has not been set, so the tile says so instead of accepting a message it cannot
   deliver. The button keeps its place and its label; it stops being a promise. */
.tile-contact-form[data-form-unconfigured] [data-role="submit"] { opacity: .55; cursor: not-allowed; }
/* The mailto fallback: the composed message, offered when the visitor's device has no mail
   handler to receive the handoff. `pre` because the newlines are the message's shape, and
   `user-select: all` so one tap selects the whole thing on a phone — where copying by drag is
   not available and the selection path in contact-form.js is the only route. */
.tile-contact-form__fallback { display: grid; gap: var(--space-2); }
.tile-contact-form__fallback[hidden] { display: none; }
.tile-contact-form__fallback-note { margin: 0; font-size: .9rem; color: var(--color-text-muted); }
.tile-contact-form__composed {
  margin: 0;
  padding: var(--space-3);
  border: 1px solid var(--color-surface-2);
  border-radius: var(--radius-md);
  background: var(--color-background);
  color: var(--color-text);
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: .82rem;
  line-height: 1.5;
  white-space: pre-wrap;      /* the message must not force the page sideways on a phone */
  overflow-wrap: break-word;
  user-select: all;
  max-height: 12rem;
  overflow-y: auto;
}

/* --------------------------------------------------------------------------
   8. Footer tile
   -------------------------------------------------------------------------- */
.tile-footer {
  padding-block: var(--space-12) var(--space-8);
  display: grid;
  gap: var(--space-8);
  color: var(--color-text-muted);
}
.tile-footer a { color: var(--color-text); text-decoration: none; }
/* `accent-text`, not `accent`: the accent is a brass FILL and measures 1.7:1 as text. This is
   the one place in the framework where the accent is written rather than painted — and it was
   failing AA before the split (4.04:1), with no gate to notice. */
.tile-footer a:hover { color: var(--color-accent-text); }
.tile-footer__brand { font-family: var(--font-display); font-weight: 700; font-size: 1.25rem; color: var(--color-text); }
.tile-footer__cols { display: flex; flex-wrap: wrap; gap: var(--space-2) var(--space-5); align-items: center; }
.tile-footer__col h4 { font-size: .85rem; text-transform: uppercase; letter-spacing: .06em; color: var(--color-text-muted); }
.tile-footer__legal { font-size: .85rem; border-top: 1px solid var(--color-surface-2); padding-top: var(--space-4); display: flex; justify-content: space-between; gap: var(--space-4); flex-wrap: wrap; }

/* ---- variant: simple ------------------------------------------------------
   The full footer cut down to one row — brand and links side by side, legal
   underneath. Same content, roughly half the height, for a site whose footer is
   a formality rather than a navigation surface. */
.tile-footer.tile-footer--simple {
  padding-block: var(--space-8) var(--space-6);
  gap: var(--space-5);
  grid-template-columns: 1fr auto;
  align-items: center;
}
.tile-footer.tile-footer--simple .tile-footer__legal {
  border-top: 0;
  padding-top: 0;
  grid-column: 1 / -1;
  justify-content: flex-start;
  gap: var(--space-5);
}

/* ---- variant: minimal -----------------------------------------------------
   The legal line and nothing else, centred.

   The brand and the links stay in the DOM and are hidden by clip, exactly as the
   minimal nav does, because a footer is a crawl path: removing a site's link
   destinations from the served HTML to make it look tidy costs real SEO. Hidden
   is not the same as absent, and check-site still sees them. */
.tile-footer.tile-footer--minimal {
  padding-block: var(--space-6);
  gap: 0;
  text-align: center;
}
.tile-footer.tile-footer--minimal > div:first-child,
.tile-footer.tile-footer--minimal .tile-footer__cols {
  position: absolute; width: 1px; height: 1px; overflow: hidden;
  clip-path: inset(50%); white-space: nowrap;
  padding: 0; margin: 0; border: 0;
}
.tile-footer.tile-footer--minimal .tile-footer__legal {
  border-top: 0;
  padding-top: 0;
  justify-content: center;
}

/* --------------------------------------------------------------------------
   9. CTA band tile
   -------------------------------------------------------------------------- */
.tile-cta { display: grid; }
.tile-cta__inner { display: grid; gap: var(--space-4); }
.tile-cta--center .tile-cta__inner { text-align: center; justify-items: center; }
.tile-cta--center .tile-hero__actions { justify-content: center; }

/* --------------------------------------------------------------------------
   10. Announcement strip
   -------------------------------------------------------------------------- */
/* The ONE tile that is deliberately not a unit. It spans the viewport by design, so it
   takes no inset from the screen edge, no boundary and no radius — the opposite of the
   tile unit above, which is why the resets are explicit rather than omitted. It outranks
   the unit by source order (equal specificity, later). */
.tile-announcement { text-align: center; font-size: .85rem; font-weight: 500; padding: var(--space-2) var(--space-4); background: var(--color-accent); color: var(--color-on-accent); border: 0; border-radius: 0; box-shadow: none; }

/* --------------------------------------------------------------------------
   11. Content/speciality tiles
   -------------------------------------------------------------------------- */

/* Media & video */
.tile-media { display: grid; gap: var(--space-2); }
.tile-media figcaption { font-size: .85rem; color: var(--color-text-muted); }
/* The video tile is a STACK: a 16:9 void-black media box, then a caption BELOW it.
   The aspect ratio and the void background belong to the media box, not to the tile
   root. On the root they put the caption on black — dark navy on #000, measured at
   1.81:1 in light mode — and the root's own overflow clip would have cut off any
   caption taller than the box. `.media` already supplies the position, the clip and
   the radius; only the void colour is added here. */
.tile-video { display: grid; gap: var(--space-2); }
.tile-video > .media { border-radius: var(--radius-lg); background: var(--color-media-void); }
.tile-video__placeholder { position: absolute; inset: 0; display: grid; place-items: center; color: var(--color-on-media); font-weight: 600; cursor: default; background: var(--scrim-media); }
/* Only a tile that HAS a source offers a click. video.js returns before it binds one
   when the URL is empty, so a pointer cursor would promise something nothing honours. */
.tile-video[data-video-src]:not([data-video-src=""]) .tile-video__placeholder { cursor: pointer; }

/* Gallery */
.tile-gallery { display: grid; gap: var(--space-4); }
.tile-gallery .media { border-radius: var(--radius-md); }

/* Team */
.tile-team__links { display: flex; gap: var(--space-2); }
.tile-team__links a { display: inline-grid; place-items: center; width: 32px; height: 32px; border-radius: var(--radius-sm); border: 1px solid var(--color-surface-2); text-decoration: none; color: var(--color-text); }

/* Quote */
.tile-quote { padding: var(--space-6); border-left: 4px solid var(--color-accent); background: var(--color-surface); border-radius: var(--radius-lg); font-family: var(--font-display); font-size: 1.25rem; }
.tile-quote blockquote { margin: 0; }
.tile-quote figcaption { margin-top: var(--space-3); font-family: var(--font-sans); font-size: .9rem; color: var(--color-text-muted); }

/* Product */
/* The box comes from the tile unit; this tile only adds its layout. It used to declare
   `padding: var(--space-5)` (20px) against every sibling card's 24px, which put its inner
   content 4px out of line with the pricing card beside it in the same row. */
.tile-product { display: flex; flex-direction: column; gap: var(--space-3); }
.tile-product__meta { display: flex; justify-content: space-between; gap: var(--space-2); }
.tile-product .btn { width: 100%; }

/* Menu */
.tile-menu ul { list-style: none; padding: 0; margin: 0; }
.tile-menu li[data-role="item"] { display: flex; justify-content: space-between; gap: var(--space-4); padding: var(--space-2) 0; border-bottom: 1px dashed var(--color-surface-2); }

/* FAQ */
.tile-faq details { border: 1px solid var(--color-surface-2); border-radius: var(--radius-md); padding: var(--space-3) var(--space-4); }
.tile-faq details + details { margin-top: var(--space-2); }
.tile-faq summary { cursor: pointer; font-weight: 600; }
.tile-faq details[open] summary { color: var(--color-primary); }

/* Social */
.tile-social { display: flex; flex-wrap: wrap; gap: var(--space-2); }

/* Map */
.tile-map__canvas { aspect-ratio: 16 / 9; border-radius: var(--radius-lg); overflow: hidden; background: var(--color-surface-2); }

/* Search */
/* `minmax(0, 1fr)` and the two `min-width: 0`s are load-bearing, not tidiness.
   A grid column with no template is sized to the item's MIN-CONTENT, and a flex item's
   default minimum is its min-content size — and an `<input>`'s min-content is its intrinsic
   width (about 195px at the default `size`). So the column could not shrink below roughly
   293px however narrow the viewport was, and the row overflowed by a CONSTANT 47px at 320,
   27px at 340, 7px at 360. The tile used to sit unbounded on the page with no inset at all,
   which hid this completely; the tile unit's real 24px inset exposed it and
   `mobile-width.cjs` named the mechanism. Measured at 320 before the fix: the form was
   293px wide inside a 220px tile. */
.tile-search { display: grid; grid-template-columns: minmax(0, 1fr); gap: var(--space-3); }
.tile-search__form { display: flex; min-width: 0; gap: var(--space-2); }
/* `min-width: 0` is load-bearing, not tidiness. A flex item's default minimum is its
   min-content size, and an input's intrinsic width is wide — so `flex: 1` could not actually
   shrink it. The tile sat unbounded on the page (no unit inset at all) which hid this
   completely; the moment the tile unit gave it a real 24px inset, the row overflowed the
   viewport by 47px at 320. `mobile-width.cjs` caught it at 320/340/360. */
.tile-search__form input { flex: 1; min-width: 0; padding: var(--space-3); border: 1px solid var(--color-surface-2); border-radius: var(--radius-md); background: var(--color-background); color: var(--color-text); }
.tile-search__results { display: grid; gap: var(--space-1); }
.tile-search__result { display: block; padding: var(--space-2); border-radius: var(--radius-sm); text-decoration: none; color: var(--color-text); }
.tile-search__result:hover { background: var(--color-surface-2); }

/* Event / calendar */
/* Layout only; the box is the tile unit's. */
.tile-event { display: grid; gap: var(--space-3); }

/* --------------------------------------------------------------------------
   12. Code / snippet tile
   -------------------------------------------------------------------------- */
.tile-code { display: grid; gap: var(--space-2); }
.tile-code h3 { margin: 0; font-family: var(--font-sans); font-size: .8rem; text-transform: uppercase; letter-spacing: .07em; color: var(--color-text-muted); }
.tile-code pre {
  margin: 0;
  padding: var(--space-4);
  overflow-x: auto;
  background: var(--color-surface-2);
  border: 1px solid color-mix(in srgb, var(--color-primary) 12%, transparent);
  border-radius: var(--radius-lg);
}
.tile-code code {
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: .82rem;
  line-height: 1.6;
  color: var(--color-text);
  white-space: pre;
  tab-size: 2;
}
/* Darkened toward the page background rather than toward black, so it stays a
   token expression and follows a re-theme. */
[data-theme="dark"] .tile-code pre { background: color-mix(in srgb, var(--color-surface-2) 78%, var(--color-background)); }
/* --------------------------------------------------------------------------
   13. Compact Keywords tiles (SEO / bottom-of-funnel)

   Same container rule as the hero: the tile root is the CONTAINER, so an
   `@container` query can never match it — .tile-keyword-landing__grid is the
   element that reflows.
   -------------------------------------------------------------------------- */

/* Keyword landing — one compact keyword per page */
.tile-keyword-landing { display: grid; gap: var(--space-6); padding-block: var(--space-12); }
.tile-keyword-landing__grid { display: grid; gap: var(--space-8); align-items: start; }
.tile-keyword-landing__content { max-width: 70ch; }
.tile-keyword-landing__content h1 {
  font-size: clamp(2rem, 5vw, 3.25rem);
  margin: 0 0 var(--space-4);
  line-height: 1.15;
}
.tile-keyword-landing__content [data-role="body"] {
  font-size: var(--text-lead);
  color: var(--color-text-muted);
}
.tile-keyword-landing__content [data-role="body"] p { margin: 0 0 var(--space-4); }
.tile-keyword-landing__content [data-role="body"] p:last-child { margin-bottom: 0; }
.tile-keyword-landing__actions {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-3);
  margin-top: var(--space-6);
}
.tile-keyword-landing__media { margin: 0; }
.tile-keyword-landing__media figcaption {
  margin-top: var(--space-2);
  font-size: .85rem;
  color: var(--color-text-muted);
  text-align: center;
}
/* No image configured -> no empty box. An unset slot ships src="" (that is what
   keeps the markup a valid contract), so "configured" means a non-empty src. */
.tile-keyword-landing__media:not(:has(img[src]:not([src=""]))) { display: none; }

.tile-keyword-landing__proof {
  margin: 0;
  padding: var(--space-6);
  border-left: 4px solid var(--color-accent);
  background: var(--color-surface);
  border-radius: var(--radius-lg);
  font-family: var(--font-display);
  font-size: 1.25rem;
}
.tile-keyword-landing__proof p { margin: 0; }
.tile-keyword-landing__proof footer {
  margin-top: var(--space-3);
  font-family: var(--font-sans);
  font-size: .9rem;
  color: var(--color-text-muted);
  font-style: normal;
}
/* The proof quote is optional too — hide the block rather than leave an empty rule. */
.tile-keyword-landing__proof:not(:has([data-role="quote"]:not(:empty))) { display: none; }

@container (min-width: 45rem) {
  .tile-keyword-landing__grid:has(img[src]:not([src=""])) {
    grid-template-columns: 1.1fr .9fr;
  }
}

/* Compact keywords index — the hub of bottom-of-funnel pages */
.tile-compact-keywords { display: grid; gap: var(--space-6); }
.tile-compact-keywords__intro { max-width: 60ch; }
.tile-compact-keywords__intro h2 { margin: 0 0 var(--space-2); }
.tile-compact-keywords__intro p { margin: 0; color: var(--color-text-muted); }

.tile-keyword-card {
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
  padding: var(--space-6);
  background: var(--color-surface);
  border: 1px solid var(--color-surface-2);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-card);
}
.tile-keyword-card h3 { margin: 0; font-size: 1.15rem; }
.tile-keyword-card p { margin: 0; color: var(--color-text-muted); flex: 1; }
.tile-keyword-card .btn { align-self: flex-start; margin-top: var(--space-2); }
/* --------------------------------------------------------------------------
   14. Local-traffic tiles (visit, today)
   The plate is the CONTAINER; the __grid inside it is what reflows, because a
   container query can never match its own element.
   -------------------------------------------------------------------------- */
.tile-visit { display: grid; gap: var(--space-6); padding-block: var(--space-10); }
.tile-visit__grid { display: grid; gap: var(--space-8); align-items: start; }
.tile-visit__place h2 { margin: 0 0 var(--space-3); }
.tile-visit__address {
  font-style: normal;
  font-size: var(--text-lead);
  color: var(--color-text);
  display: block;
  max-width: 40ch;
}
.tile-visit__note { margin: var(--space-3) 0 0; color: var(--color-text-muted); }
.tile-visit__actions { display: flex; flex-wrap: wrap; gap: var(--space-3); margin-top: var(--space-6); }

.tile-visit__when h3 {
  margin: 0 0 var(--space-3);
  font-family: var(--font-sans);
  font-size: .8rem;
  text-transform: uppercase;
  letter-spacing: .07em;
  color: var(--color-text-muted);
}
/* Filled by visit.js from site.business.openingHours. Hidden until it has an
   answer, so a page with no content file simply does not show it. */
.tile-visit__state {
  display: inline-block;
  margin: 0 0 var(--space-4);
  padding: var(--space-1) var(--space-3);
  border-radius: var(--radius-pill);
  font-size: .8rem;
  font-weight: 600;
  background: var(--color-surface-2);
  color: var(--color-text);
}
.tile-visit__state[data-open="true"] {
  background: color-mix(in srgb, var(--color-primary) 14%, transparent);
  color: var(--color-primary);
}
.tile-visit__state[hidden] { display: none; }

.tile-visit__hours { list-style: none; margin: 0; padding: 0; }
.tile-visit__hours li {
  display: flex;
  justify-content: space-between;
  gap: var(--space-4);
  padding: var(--space-2) 0;
  border-bottom: 1px solid var(--color-surface-2);
  font-variant-numeric: tabular-nums;
}
.tile-visit__hours li:last-child { border-bottom: 0; }

@container (min-width: 45rem) {
  .tile-visit__grid { grid-template-columns: 1.2fr .8fr; }
}
/* Products showcase — cards that link out to where the business already sells. */
.tile-products { display: grid; gap: var(--space-6); }
.tile-products__intro { max-width: 60ch; }
.tile-products__intro h2 { margin: 0 0 var(--space-2); }
.tile-products__intro p { margin: 0; color: var(--color-text-muted); }
.tile-products__note { margin: 0; font-size: .85rem; color: var(--color-text-muted); }

.tile-product-card {
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
  padding: var(--space-5);
  background: var(--color-surface);
  border: 1px solid var(--color-surface-2);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-card);
}
.tile-product-card h3 { margin: 0; font-size: 1.05rem; }
.tile-product-card__price {
  margin: 0;
  font-weight: 700;
  color: var(--color-primary);
  font-variant-numeric: tabular-nums;
  flex: 1;
}
.tile-product-card .btn { align-self: flex-start; margin-top: var(--space-2); }