/* base css file — desktop first.

MIGRATION NOTE
This file is the target of an incremental rewrite of style.css. It loads after
style.css, so anything here wins — but the migration does not rely on that.
When a component moves here its old rules are DELETED from style.css, so each
component has exactly one home and no override fights or !important are needed.

Migrated so far:
  - the site header (.siteHeaderCont and everything it encloses)
  - every homepage block inside .bodyCont (.allBlocksHome)
  - the blog and video index rows (.allIndexRows)
  - the gallery and scene index tiles (.allThumbTiles)
  - the blog and video preview pages (.articleHead, .prevNextCont)
  - the site footer (.siteFooterCont and everything it encloses)
  - the category index pages /recommended, /itemcategory/ and /locationcategory/
  - the network strip in footer_network.ejs, which had its own <style> block
  - the item and location detail pages (.itemIndexRow / .locationIndexRow)
*/


/* DESIGN TOKENS
Every value that repeats across components lives here, so a site-wide change is
one edit. Per-site themes override the token, not the component — e.g.
familyguy.css can restyle every header surface with:
  :root { --surfaceHeader: #b93a35; }
*/
:root {
  /* Surfaces */
  --surfaceHeader: #6c757d;
  --surfaceRaised: #e9ecef;
  --surfaceRaisedActive: #cccccc;
  --surfaceCard: #ffffff;
  --surfaceSunkenHover: #8b8989;

  /* Mid grey — the page heading bar, and the card shadow it was already the
     colour of. */
  --surfaceMuted: #a9a9a9;

  /* Translucent black behind a thumbnail tile's caption, so light and dark
     images both keep white text readable. */
  --surfaceTileDark: #000000b5;

  /* Solid black — the network cards and the Recommended title bar. */
  --surfaceInk: #000000;

  /* The pale blue pair on the category index pages. */
  --surfaceTint: #a7c7e73d;
  --surfaceAccent: #8bbafe;

  /* Text */
  --textOnHeader: #ffffff;
  --textLink: #024dbc;
  --textBody: #000000;

  /* Borders */
  --borderRaised: 1px solid var(--surfaceRaisedActive);
  --borderOnHeader: 2px solid #ffffff;
  --borderStrong: 2px solid #000000;

  /* Radii */
  --radiusSm: 5px;
  --radiusMd: 10px;
  --radiusLg: 20px;

  /* Shadows */
  --shadowCard: 4px 4px var(--surfaceMuted);

  /* Space — 4px scale, in rem so it tracks the user's browser font size */
  --space1: 0.25rem;
  --space2: 0.5rem;
  --space3: 0.75rem;
  --space4: 1rem;

  /* Layout */
  --contentMaxWidth: 1000px;
  --contentGutter: var(--space2);

  /* Cards. Thumbnail frames are shared by the homepage tiles and the index rows. */
  --mediaFrameMaxWidth: 320px;
  --mediaFrameSquareSize: 100px;

  /* Grid tiles — the homepage blocks and the gallery and scene indexes. The media
     queries change the column count and nothing else; width is derived from it. */
  --cardColumns: 3;
  --cardGap: var(--space2);
  --homeTileMinHeight: 350px;

  /* Category tiles. One tile component at three sizes: the category index
     pages, the network cards, and the footer's Recommended strip. Only the
     width changes — see CATEGORY TILES. */
  --catTileSize: 290px;
  --netTileSize: 240px;
  --footerTileSize: 100px;

  /* Footer. The nav cap keeps the pills near the 80px the footer buttons used
     to be instead of stretching them across a desktop.

     The Recommended block is exactly seven tiles wide: seven tiles, the six
     gaps between them, the grid's padding either side and the block's border.
     Seven is the most categories that are ever flagged useInFooter, so the
     block is capped at its own widest possible content and can never open up
     wider than the tiles inside it. Raise the 7 if more are ever added — the
     rest of the arithmetic follows the tokens. */
  --footerNavMaxWidth: 32rem;
  --footerRecommendedMaxWidth: calc(
    var(--footerTileSize) * 7 + var(--space2) * 8 + 2px
  );

  /* Four network cards on a desktop: 4 x 240 + 3 gaps + padding + border. Wider
     than --contentMaxWidth on purpose — eight cards divide evenly into four. */
  --netGridMaxWidth: 1024px;

  /* Interaction */
  --pillMinHeight: 2rem;
  --tapTargetMin: 44px;
  --focusRing: 3px solid #ffbf47;
  --transitionFast: 120ms ease-in-out;
}


/* REUSABLE PRIMITIVES
Layout and appearance patterns with no component-specific knowledge. Compose
these in markup (class="siteHeaderNav pillBar") instead of restating padding,
radius and hover colours per component.
*/

/* Centered page-width band. Replaces the fixed pixel widths the old media
   queries handed out per breakpoint (300 / 650 / 940 / 1000px) — one rule now
   covers every viewport. The gutter keeps it off the screen edge at widths just
   past the max-width. Add a component to this selector list rather than
   restating the three properties. */
.contentBand,
.siteHeaderCont {
  box-sizing: border-box;
  width: calc(100% - (var(--contentGutter) * 2));
  max-width: var(--contentMaxWidth);
  margin-inline: auto;
}

/* The grey bar naming a listing page — "Restaurants", "Gear". Was
   .itemHeader / .locationHeader, two selectors with identical declarations and
   a flat width:290px, so a one-word category got 290px of bar and a long one
   wrapped inside it. fit-content sizes the bar to the name and the max-width
   keeps it on the screen.

   It sits on an <h2>, so the margins are stated here rather than left to the
   browser's default heading margin. */
.pageHeading {
  box-sizing: border-box;
  width: fit-content;
  max-width: min(var(--contentMaxWidth), 100% - var(--contentGutter) * 2);
  margin: var(--space2) auto;
  padding: var(--space2) var(--space4);
  border-radius: var(--radiusMd);
  background-color: var(--surfaceMuted);
  color: var(--textBody);
  font-size: 20px;
  font-weight: bold;
  text-align: center;
}

/* Row of items that wraps instead of overflowing. Replaces float + clear. */
.pillBar {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space1);
  margin: 0;
  padding: 0;
  list-style: none;
}

/* Button-shaped nav link.

   Sizing is content-driven: flex-basis auto means a pill's share of the row
   starts at its label width, so a row of pills only wraps once the labels
   genuinely do not fit. There is deliberately no min-width — a floor here is
   what forced three links onto three separate rows on a 320px phone, since a
   min-width large enough to look balanced on a five-link site exceeds the space
   three links have to share.

   overflow-wrap is break-word rather than anywhere: anywhere also lets a pill
   shrink narrower than its longest label, which chops words mid-letter. */
.navPill {
  box-sizing: border-box;
  display: flex;
  flex: 1 1 auto;
  align-items: center;
  justify-content: center;
  min-height: var(--pillMinHeight);
  padding: var(--space1) var(--space2);
  border: var(--borderRaised);
  border-radius: var(--radiusSm);
  background-color: var(--surfaceRaised);
  color: var(--textLink);
  font-weight: bold;
  line-height: 1.2;
  text-align: center;
  text-decoration: none;
  overflow-wrap: break-word;
  transition: background-color var(--transitionFast);
}

.navPill:hover {
  background-color: var(--surfaceRaisedActive);
}

/* Admin header pills. Same tokens, but their own layout: the admin header is a
   float grid of fixed 200px boxes, so it does not share the nav's flex sizing
   and changes to .navPill cannot affect admin pages. */
.adminSiteHeaderLinkCont {
  box-sizing: border-box;
  display: block;
  float: left;
  width: 200px;
  min-height: var(--pillMinHeight);
  margin: 1px;
  padding: var(--space1) var(--space2);
  border: var(--borderRaised);
  border-radius: var(--radiusSm);
  background-color: var(--surfaceRaised);
  color: var(--textLink);
  text-align: center;
  text-decoration: none;
  transition: background-color var(--transitionFast);
}

.adminSiteHeaderLinkCont:hover {
  background-color: var(--surfaceRaisedActive);
}

/* Current page. aria-current is the source of truth so the styling and the
   accessibility hint can never drift apart. .activeHeaderLink is the older
   class-based equivalent, still used by admin views. */
.navPill[aria-current],
.activeHeaderLink {
  background-color: var(--surfaceRaisedActive);
}

/* Keyboard focus, on any interactive element inside a header surface. */
.navPill:focus-visible,
.adminSiteHeaderLinkCont:focus-visible,
.siteHeaderTitle:focus-visible {
  outline: var(--focusRing);
  outline-offset: 2px;
}


/* SITE HEADER
Two flex tracks — brand and nav — in a wrapping row. Each track declares the
width it needs to stay readable (flex-basis) and wraps to its own line when the
viewport cannot give it that, so the header reflows continuously instead of
snapping between breakpoints. Its width comes from .contentBand above.
*/
.siteHeaderCont {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-start;
  justify-content: space-between;
  gap: var(--space2) var(--space3);
  margin-block: var(--space1);
  padding: var(--space2);
  border: var(--borderOnHeader);
  border-radius: var(--radiusSm);
  background-color: var(--surfaceHeader);
  color: var(--textOnHeader);
}

.siteHeaderBrand {
  flex: 1 1 14rem;
  min-width: 0;
}

/* Two sizes, set in plain px and stepped down in the phone block below. Replaces
   the old siteHeaderLink / 20 / 18 / 16 / 14 ladder, which picked one of five
   classes from the site name's length and then set 20px for all of them.

   No overflow-wrap here on purpose. A domain is a single unbreakable word, and
   the default (normal) means the browser will never split one across two lines —
   so "JimMorrisonTheDoors.com" cannot drop its last letter onto a second row.
   Names with spaces still wrap between words. The trade-off is that a single
   word longer than the header can overflow it; if a domain longer than about 25
   characters is ever added, lower the phone font-size below instead of allowing
   the break. */
.siteHeaderTitle {
  display: inline-block;
  color: var(--textOnHeader);
  font-size: 20px;
  font-weight: bold;
  line-height: 1.2;
  text-decoration: none;
}

.siteHeaderTitle:hover {
  color: var(--textOnHeader);
  text-decoration: underline;
}

/* Names of 20 characters or more (set in headerpreview.ejs) start a step down. */
.siteHeaderTitleLong {
  font-size: 18px;
}

.siteDesc {
  margin: var(--space1) 0 0;
  color: var(--textOnHeader);
  font-size: 15px;
  font-weight: normal;
}

.siteHeaderNav {
  display: flex;
  flex: 1 1 20rem;
  flex-direction: column;
  gap: var(--space1);
  min-width: 0;
}


/* CARD PARTS
The site draws the same card in two shapes: a grid tile on the homepage
(.allBlocksHome) and a full-width row on the blog and video index pages
(.allIndexRows). The parts inside the card are identical in both, so they are
named for the card rather than the page — .cardHeader, .cardBody, .cardTitle and
so on. A new page that needs this card composes it from these and adds one rule
for whatever it genuinely does differently.

Before this, each place hand-rolled its own names for the same jobs:
.blogSummaryThumbCont, .videoSummaryThumbHomeCont, .gallerySummaryThumbHomeCont,
.sceneImgContHomepage, .blurbImgContHomepage, .quizImgSrc, .videoSummaryThumbCont
and .boxText were eight names for "the thumbnail or text area of a card", each
with its own media queries.
*/

/* The card's outer shell — every shape shares its appearance and differs only in
   how it sizes and lays out, below. */
.allBlocksHome,
.allThumbTiles,
.allIndexRows {
  box-sizing: border-box;
  border: var(--borderStrong);

  /* Square top corners meet the header bar; rounded bottom, as before. */
  border-radius: 0 0 var(--radiusMd) var(--radiusMd);
  box-shadow: var(--shadowCard);
  background-color: var(--surfaceCard);
  overflow: hidden;
}

/* A wrapping grid of tiles. Goes on the container — .bodyCont on the homepage,
   .galleryBodyCont on the gallery index.

   Wrapping is explicit here rather than emergent from float geometry: a float
   only drops to the next line when it finds horizontal room, so tiles whose
   bottom margins differ by even 1px clear at different heights and every later
   float lands in the wrong column, leaving holes. Flex wrapping packs each line
   independently, and the default align-items:stretch also makes every tile on a
   line match the tallest one. */
.cardGrid {
  display: flex;
  flex-wrap: wrap;
  align-content: flex-start;
}

/* Anything in the grid that is not a tile spans it rather than being laid out as
   one more tile — the "tagged with #x" line and the <h6> above the tiles, and the
   pagination nav below them. */
.cardGrid > :not(.allBlocksHome):not(.allThumbTiles) {
  flex: 0 0 100%;
}

/* Tile geometry, shared by the homepage blocks and the gallery index. Width comes
   from the column count so the media queries only ever change one number. */
.allBlocksHome,
.allThumbTiles {
  display: flex;
  flex: 0 0 auto;
  flex-direction: column;

  /* Ignored inside .cardGrid; keeps a tile usable on its own. */
  float: left;
  width: calc((100% / var(--cardColumns)) - var(--cardGap));
  margin: 0 calc(var(--cardGap) / 2) var(--cardGap);
}

/* Grey title bar. Reuses the site header's surface token, so a theme that
   restyles --surfaceHeader restyles these too. */
.cardHeader {
  flex: 0 0 auto;
  padding: var(--space2);
  background-color: var(--surfaceHeader);
  color: var(--textOnHeader);
  font-weight: bold;
  text-align: left;
}

.cardHeaderLink {
  color: var(--textOnHeader);
  font-weight: bold;
  text-decoration: none;
}

.cardHeaderLink:hover {
  color: var(--textOnHeader);
  text-decoration: underline;
}

/* The middle region. On a homepage tile flex:1 makes it absorb the spare height,
   which is what pushes .cardFooterLink to the bottom edge; on an index row there
   is no spare height and it simply wraps the content. overflow:hidden contains a
   floated thumbnail, replacing the clear:both divs this markup used to need. */
.cardBody {
  flex: 1 1 auto;
  padding: var(--space2);
  overflow: hidden;
}

/* Thumbnail frame. The default is a wide banner; the cards that want a small
   square beside the text override it below. */
.mediaFrame {
  width: 100%;
  max-width: var(--mediaFrameMaxWidth);
  aspect-ratio: 16 / 9;
  margin: 0 auto;
  overflow: hidden;
}

/* Crop to fill the frame. .videoBlockHome overrides this — it is the one card
   whose thumbnail must be shown whole rather than cropped. */
.mediaFrameImg {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

/* Cards that frame the thumbnail as a small square floated beside the text
   instead of a banner across the card. */
.blogBlockHome .mediaFrame,
.customBlockHome .mediaFrame,
.quizBlockHome .mediaFrame,
.blogIndexRow .mediaFrame,
.itemIndexRow .mediaFrame,
.locationIndexRow .mediaFrame {
  float: left;
  width: var(--mediaFrameSquareSize);
  max-width: none;
  aspect-ratio: 1 / 1;
  margin: 0 var(--space3) var(--space1) 0;
  border: 1px solid var(--textBody);
}

/* Two lines, then clip — a long title cannot push the layout around. Expressed
   in em so it follows the font size instead of a fixed 55px. */
.cardTitle {
  max-height: 2.6em;
  margin-top: var(--space1);
  overflow: hidden;
  font-weight: bold;
}

/* "Read »" / "View »" — an inline link at the end of the body text. Sets its own
   colour rather than relying on Bootstrap's .text-dark utility. */
.cardMoreLink {
  display: block;
  margin-top: var(--space1);
  color: var(--textBody);
  font-weight: bold;
}

.cardTags {
  margin-top: var(--space2);
}

/* Bottom bar, homepage tiles only. margin-top:auto is what pins it to the
   card's bottom edge. */
.cardFooterLink {
  display: block;
  margin: auto var(--space1) var(--space1);
  padding: var(--space2);
  border-radius: var(--radiusMd);
  background-color: var(--surfaceRaisedActive);
  color: var(--textBody);
  font-weight: bold;
  text-align: right;
  text-decoration: none;
  transition: background-color var(--transitionFast);
}

.cardFooterLink:hover {
  background-color: var(--surfaceSunkenHover);
  color: var(--textBody);
}

.allBlocksHome a:focus-visible,
.allIndexRows a:focus-visible {
  outline: var(--focusRing);
  outline-offset: 2px;
}


/* HOMEPAGE BLOCKS
Grid tiles. Markup pairs the shared class with a per-block one:

  <div class="allBlocksHome blogBlockHome">

The block classes are .blogBlockHome, .videoBlockHome, .galleryBlockHome,
.customBlockHome, .linksBlockHome, .quizBlockHome, .sceneBlockHome and
.blurbBlockHome. A tile with no block class (the "nothing published yet"
placeholders) is valid and renders as the plain shared card.

The tile is a flex column — header, body, footer link — which is what lets it use
min-height rather than the fixed height it had before, so content taller than the
card no longer gets clipped.
*/

/* A uniform tile height per row comes free from the grid's align-items:stretch,
   so this floor only exists to give the homepage a consistent look when a row is
   short. Set it to 0 to let every tile size to its content. */
.allBlocksHome {
  min-height: var(--homeTileMinHeight);
}

/* Per-block rules — the complete list of ways the eight tiles differ.
   .galleryBlockHome and .sceneBlockHome need nothing: the shared banner is
   already what they want, and the square-thumbnail group is defined above. */

/* Video thumbnails are already 16:9, so fit the whole frame rather than cropping
   it the way a photo of arbitrary shape needs. */
.videoBlockHome .mediaFrameImg {
  object-fit: contain;
}

/* The blurb banner is framed; every other banner sits flush. */
.blurbBlockHome .mediaFrame {
  border: 1px solid var(--textBody);
}

/* The link list carries a list instead of a thumbnail. Trims the browser's
   default bullet indent to something that fits a narrow card. */
.linksBlockHome .cardList {
  margin: 0;
  padding-left: 1.1em;
}


/* INDEX ROWS
Full-width rows on /blog/index and /video/index, which were near-identical
templates. Markup pairs the shared class with a per-page one:

  <div class="allIndexRows blogIndexRow">

.blogIndexRow used to carry the whole card and was reused as a generic
"full-width card" by the tag list and the item and location pages, none of which
are blog rows. Those now use .allIndexRows on its own, and .blogIndexRow means
only what is specific to a blog row.
*/
.allIndexRows {
  width: 100%;
  margin: 0 0 var(--space3);
}

/* Per-page rules. Blog rows use the shared square thumbnail defined above; a
   video row's thumbnail is a 16:9 still, so it keeps the shared aspect ratio and
   only changes the size and position. The video preview page frames its
   thumbnail the same way, so it shares this rule. */
.videoIndexRow .mediaFrame,
.videoArticleHead .mediaFrame {
  float: left;
  width: 160px;
  max-width: none;
  margin: 0 var(--space2) var(--space1) 0;
  border: 1px solid var(--textBody);
}


/* THUMBNAIL TILES
Grid tiles on /gallery/index and /scene/index, which drew byte-identical cards
from the same classes. It is the homepage tile with a different emphasis — the
thumbnail is the content rather than an illustration next to text — so the shell,
geometry, header, body and frame all come from above and only these three rules
are specific.

One class covers both pages because the two tiles are the same thing; the name
describes the tile rather than either page. Add a per-page class only if they ever
need to differ.

This retired .galleryIndexRow and .galleryIndexSummary. .galleryThumbImgCont and
.galleryThumbImg stay in style.css — the gallery preview and form pages and the
scene watch body still use them for their own thumbnails.
*/

/* The thumbnail is the point of these pages, so it sits centred with the caption
   under it rather than floated beside text. */
.allThumbTiles .cardBody {
  text-align: center;
}

/* Black backing, because the image is fitted whole rather than cropped and a
   thumbnail can be any shape — the letterboxing is deliberate. */
.allThumbTiles .mediaFrame {
  background-color: var(--textBody);
  border: 1px solid var(--textBody);
}

.allThumbTiles .mediaFrameImg {
  object-fit: contain;
}


/* ARTICLE PREVIEW PAGES
/blog/preview and /video/preview. These are detail pages rather than lists, so
they share less than the index pages did — but they open the same way: a title,
then a lead paragraph, optionally with a thumbnail beside it. That much is shared
as .articleHead / .articleTitle / .articleDesc, paired with a page class:

  <div class="articleHead blogArticleHead">

.blogTitleCont, .blogTitle and .youtubeDesc had no rules at all before — they were
classes in the markup that no stylesheet ever matched, which is why the two pages'
lead paragraphs were spaced differently for no reason.
*/

/* overflow contains the video page's floated thumbnail, replacing the clear:both
   divs the markup used to need. */
.articleHead {
  margin-bottom: var(--space2);
  overflow: hidden;
}

/* Sits on an <h3>, so the size still comes from the heading. */
.articleTitle {
  margin-bottom: var(--space2);
}

.articleDesc {
  padding: var(--space2) var(--space2) var(--space2) 0;
}

/* Italic on the blog page only, matching what .blogDescBeforeBody did. A video
   description is often several paragraphs of YouTube copy, which reads badly
   fully italicised. */
.blogArticleHead .articleDesc {
  font-style: italic;
}

.articleBody {
  position: relative;
}

/* Was three media queries capping this at 800 / 650 / 260px. A percentage cap
   does the same job at every width and cannot be outgrown by a wider container. */
.articleBody p img,
.articleBody div img {
  display: block;
  max-width: 100%;
  height: auto;
  margin: 0 auto;
}

.articleBody p {
  margin-bottom: 0;
}

/* Embedded Instagram blockquote. min() replaces a fixed 540px plus a 326px phone
   override. */
.articleEmbedCont {
  width: min(540px, 100%);
  margin: 0 auto;
}


/* Previous / next pager at the foot of a blog post. Two equal columns that become
   one on a phone. Replaces float + a 48% width + a fixed 80px height that clipped
   longer post titles. */
/* Width matches .blogBodyCont above it so the pager lines up with the article
   rather than with the viewport. */
.prevNextCont {
  box-sizing: border-box;
  display: flex;
  flex-wrap: wrap;
  gap: var(--space1);
  width: 100%;
  max-width: var(--contentMaxWidth);
  margin: var(--space1) auto 0;
  padding: 0;
}

.prevNextItem {
  box-sizing: border-box;
  display: flex;
  flex: 1 1 14rem;
  align-items: center;
  gap: var(--space2);
  min-width: 0;
  padding: var(--space2);
  border: 1px solid var(--textBody);
  border-radius: var(--radiusSm);
  background-color: #fafafa;
  color: var(--textBody);
  font-weight: bold;
  text-decoration: none;
  overflow: hidden;
}

.prevNextItem:hover {
  background-color: var(--surfaceRaised);
  color: var(--textBody);
}

.prevNextItem:focus-visible {
  outline: var(--focusRing);
  outline-offset: 2px;
}

/* "Next" reads right-to-left: label, then thumbnail, then arrow. */
.prevNextNext {
  flex-direction: row-reverse;
  text-align: right;
}

.prevNextThumb {
  flex: 0 0 auto;
  height: 60px;
  border: 1px solid var(--textBody);
}

.prevNextArrow {
  flex: 0 0 auto;
  height: 60px;
}

/* The label is the only part allowed to shrink and wrap. Three lines then clip:
   the old fixed 80px card height cut long post titles off mid-word, but letting
   them run free took the pager to over 300px tall on a tablet. */
.prevNextLabel {
  max-height: 3.9em;
  min-width: 0;
  overflow: hidden;
}


/* ITEM AND LOCATION PAGES
/item/page and /location/page — a listing of shops or places, one card each.
The two templates were byte-identical apart from the words "item" and
"location" in every class name, so they had two of everything: .itemLinkCont
and .locationLinkCont, .itemImageCont and .locationImageCont, .itemHeader and
.locationHeader. Each pair had one rule with both selectors in it, which is the
tell that they were one component all along.

The card itself is .allIndexRows + .cardHeader + .cardBody from CARD PARTS, and
the thumbnail is the square .mediaFrame the blog rows use, so nothing here
restates the card. Only the row of links below the description is specific, and
it is named for what it is rather than for either page.

That row was a float line of fixed 240px boxes, 60px tall, with the label set
white-space:nowrap and text-overflow:ellipsis — so "Reservations at The Butcher's
Daughter" was cut to "Reservations at The Butch…" and a 50px icon was clipped to
44px by the box's own overflow:hidden. Two media queries then restated the width
as 180px on a tablet and the map button as 240px on a phone. A wrapping flex row
of content-sized buttons needs none of that: the label wraps instead of being
cut, the button is as wide as it needs to be, and the row wraps when it runs out
of space.
*/

/* Extends .pillBar. The gap is the pill gap doubled — these are large buttons
   with icons, and at 4px they read as one continuous grey band.

   clear:left puts the bar below the thumbnail rather than beside it. A flex
   container is a block formatting context, so without this the whole bar is
   pushed into whatever channel is left next to the floated thumbnail — and on a
   card with a one-line description that channel is about 160px, narrow enough
   that "On Amazon.com" broke across two lines mid-word. The description still
   wraps around the thumbnail as before; only the buttons clear it. */
.iconLinkBar {
  clear: left;
  gap: var(--space2);
  margin-top: var(--space2);
}

/* flex: 0 1 auto — a button holds its content width and only shrinks when it is
   alone on a line and still too wide, which is the same sizing .catTile uses.
   min-height is the touch target rather than the old fixed 60px, so the button
   grows when a long label wraps to two lines. */
.iconLink {
  box-sizing: border-box;
  display: flex;
  flex: 0 1 auto;
  align-items: center;
  gap: var(--space2);
  max-width: 100%;
  min-height: var(--tapTargetMin);
  padding: var(--space2);
  border-radius: var(--radiusMd);
  background-color: var(--surfaceRaised);
  color: var(--textBody);
  font-weight: bold;
  line-height: 1.2;
  text-decoration: none;
  transition: background-color var(--transitionFast);
}

.iconLink:hover {
  background-color: var(--surfaceRaisedActive);
  color: var(--textBody);
}

.iconLink:focus-visible {
  outline: var(--focusRing);
  outline-offset: 2px;
}

/* Was .www_image, a bare width:50px that any page could pick up. */
.iconLinkIcon {
  flex: 0 0 auto;
  width: 50px;
  height: auto;
}

/* The only part that shrinks. min-width:0 is what allows it to — a flex item's
   default min-width:auto floors it at its longest word, which is what pushes a
   long label past the edge of the screen instead of wrapping it. */
.iconLinkLabel {
  min-width: 0;
  overflow-wrap: break-word;
}


/* CATEGORY TILES
One image-over-caption tile, used at three sizes by four templates:

  .catTile                 pages/recommended, pages/itemcategory,
                           pages/locationcategory   (290px, was .catLargeButtonCont)
  .catTile .netFooterTile  partials/footer_network  (240px, was .NFcont)
  .catTile .footerTile     partials/footerads       (100px, was .thumbFooter)

They were three separate components with three sets of fixed pixel geometry, and
each one sat in a container whose width was arithmetic on the tile count. The
size modifiers below change a width and nothing else; everything structural —
the flex column, the square frame, the hover and focus treatment — is stated
once here.

.catTileGrid is a wrapping flex row, so the number of columns is whatever fits.
That replaces four fixed container widths and five media queries that restated
them. A tile is flex: 0 1 auto, so it holds its width until it is the only thing
on a line and still does not fit — on a 320px phone it shrinks the few pixels
needed instead of overflowing. That is deliberately not a percentage width: the
grid sits in a fit-content container, and a percentage there resolves against a
width that is itself being measured, which made the same tile come out 290px on
one page and 271px on another.
*/

.catTileGrid {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: var(--space2);
  padding: var(--space2);
}

/* Height follows the caption instead of a fixed height that clipped longer
   category names; the grid's default align-items:stretch still levels every tile
   on a line to the tallest one. */
.catTile {
  box-sizing: border-box;
  display: flex;
  flex: 0 1 auto;
  flex-direction: column;
  width: var(--catTileSize);
  border: 1px solid var(--textBody);
  background-color: var(--surfaceTileDark);
  color: var(--textOnHeader);
  line-height: 1.2;
  text-align: center;
  text-decoration: none;
  overflow: hidden;
}

.catTile:hover {
  color: var(--textOnHeader);
  text-decoration: underline;
}

.catTile:focus-visible {
  outline: var(--focusRing);
  outline-offset: -3px;
}

/* Square and edge to edge, where the shared .mediaFrame is a 16:9 banner capped
   at 320px. object-fit on .mediaFrameImg crops to fill, so a category image of
   any shape fills the square instead of leaving a gap beside it. */
.catTileFrame {
  display: block;
  flex: 0 0 auto;
  max-width: none;
  aspect-ratio: 1 / 1;
}

.catTileName {
  padding: var(--space2);
}


/* CATEGORY INDEX PAGES
/recommended, /itemcategory/ and /locationcategory/, which are the same page
with a different set of tiles. /recommended prints a header bar per category
type and the other two print a single one.

.catBodyCont was max-width:898px with three media queries narrowing it, plus an
inline style="width:600px" the templates wrote when the category count was small.
Because a tile is 292px wide with an 8px gap, 898px fitted two tiles with 300px
of dead space beside them and the 598px breakpoints fitted exactly one. It is
now fit-content over a wrapping grid: three across on a desktop, two on a
tablet, one on a phone, with no width in the markup and no breakpoint here.
*/

.catBodyCont {
  box-sizing: border-box;
  width: fit-content;
  max-width: min(var(--contentMaxWidth), 100% - var(--contentGutter) * 2);
  margin: var(--space2) auto;
  border: 1px solid var(--textBody);
  border-radius: var(--radiusMd);
  background-color: var(--surfaceTint);
  text-align: center;
}

/* One rule for what were .recommendedItemHeader and .recommendedLocationHeader,
   two selectors with identical declarations. Both classes stay in the markup so
   a theme can still target one type. */
.catIndexHeader {
  box-sizing: border-box;
  max-width: 280px;
  margin: var(--space3) auto;
  padding: var(--space2);
  border: 1px solid var(--textBody);
  border-radius: var(--radiusSm);
  background-color: var(--surfaceAccent);
  font-weight: bold;
  text-align: center;
}


/* FOOTER NETWORK
partials/footer_network.ejs, the "GcGz Network" strip of sister-site cards. It
carried its own <style> block, which shipped on every page of every site, and
its cards were .NFcont: a 100px-tall box with the title absolutely positioned
over the top of it and the image absolutely positioned at top:36px, so all that
showed of a 240px-wide image was a 64px sliver. The container was 750px, which
fitted two 251px cards and left the third column empty.

The cards are .catTile at 240px now, so the image is a whole square and the row
count is whatever fits.
*/

.netFooterCont {
  box-sizing: border-box;
  width: fit-content;
  max-width: min(var(--netGridMaxWidth), 100% - var(--contentGutter) * 2);
  margin: var(--space3) auto;
  border: 1px solid var(--textBody);
  border-top-left-radius: var(--radiusLg);
  border-top-right-radius: var(--radiusLg);
  /*background-color: var(--surfaceTint);*/
  background-color: #5294d63d;

  overflow: hidden;
}

.netFooterHeader {
  margin: 0;
  padding: var(--space1) var(--space2);
  background-color: var(--surfaceInk);
  color: var(--textOnHeader);
  font-size: 24px;
  font-weight: bold;
  text-align: center;
}

.netFooterTile {
  width: var(--netTileSize);
  background-color: var(--surfaceInk);
}

/* These images are site banners, not category thumbnails, so they get a banner
   frame instead of .catTileFrame's square — a square crop of a banner is mostly
   the middle third of it. */
.netFooterTile .catTileFrame {
  aspect-ratio: 16 / 9;
}

/* The title sat in a bar over the image; keeping the bar keeps the look, and as
   a flex item in normal flow it no longer has to be positioned. */
.netFooterTileTitle {
  padding: var(--space1);
  background-color: var(--surfaceInk);
  font-size: 20px;
  font-weight: bold;
}

/* The "coming soon" card has no image, so it needs the height the frame would
   otherwise give it. */
.netFooterTileNote {
  justify-content: center;
  min-height: calc(var(--netTileSize) * 9 / 16);
}


/* SITE FOOTER
Everything under the page content, wrapped in one <footer class="siteFooterCont">
by footer.ejs: the nav bar, the "Recommended" category tiles from footerads.ejs,
the social icons and the two note boxes.

Three of these widths used to be arithmetic on a count rather than a measurement:

  .footerNav / .noTagListBtnFooterNav   500px, or 400px when the Tags button was
                                        hidden — a class picked by link count
  .thumbFooterCont                      an inline max-width of tiles x 103px,
                                        written into the markup by footerads.ejs
  .socialMediaLinksBlock                an inline width of icons x 63px

Each is now a flex row that measures itself, so adding a nav link, a category or
a social icon needs no arithmetic in any template. That is also why this section
adds no media queries and deletes four: nothing here has a width to restate per
breakpoint.

The category tiles are .catTile at its smallest size, and reuse .mediaFrame /
.mediaFrameImg from CARD PARTS in place of .catImageSmallCont / .catImageSmall,
which were a 94px box with a 90px image floated inside it.

Still in style.css: .catImageLargeCont / .catImageLarge, which are also the
upload preview on itemcategory/edit and locationcategory/edit and are written
into the DOM by imageuploadmodal.ejs, so they stay where those pages expect them.
.catSmallButtonCont, .catImageSmallCont, .catImageSmall and .catLink now style
nothing but two display:none blocks on those same two edit pages.
*/

/* The page above can end on a float, which is what the clear:both divs between
   every footer block were for. One rule replaces all six. */
.siteFooterCont {
  clear: both;
  padding-bottom: var(--space4);
}

/* .pillBar and .navPill come from REUSABLE PRIMITIVES, so these are the header's
   links in a different place — same geometry, same focus ring, same 44px touch
   target on a touchscreen. */
.siteFooterNav {
  justify-content: center;
  width: min(var(--footerNavMaxWidth), 100% - var(--contentGutter) * 2);
  margin: var(--space3) auto;
}

/* Keeps the white surface and black text the footer buttons had, over the shared
   pill geometry. The hover colour still comes from .navPill:hover. */
.footerPill {
  border-color: var(--textBody);
  background-color: var(--surfaceCard);
  color: var(--textBody);
}

.footerPill:hover {
  color: var(--textBody);
}

/* fit-content is the browser doing what footerads.ejs used to compute: as wide
   as the tiles need on one line, capped at the old 1400px, never wider than the
   screen. Past the cap the tiles simply wrap. */
.footerRecommended {
  box-sizing: border-box;
  width: fit-content;
  max-width: min(var(--footerRecommendedMaxWidth), 100% - var(--contentGutter) * 2);
  margin: var(--space3) auto;
  border: 1px solid var(--textBody);
  border-radius: var(--radiusSm);
  background-color: var(--surfaceHeader);
  overflow: hidden;
}

/* The whole bar is the link, rather than an <a> wrapped around a <div>. */
.footerRecommendedHeader {
  display: block;
  padding: var(--space1) var(--space2);
  background-color: var(--textBody);
  color: var(--textOnHeader);
  font-weight: bold;
  text-align: center;
  text-decoration: none;
}

.footerRecommendedHeader:hover {
  color: var(--textOnHeader);
  text-decoration: underline;
}

.footerRecommendedHeader:focus-visible {
  outline: var(--focusRing);
  outline-offset: -3px;
}

/* The smallest of the three .catTile sizes — see CATEGORY TILES. Everything
   else about the tile, including the fixed 150px height that used to clip
   longer category names, is handled there. */
.footerTile {
  width: var(--footerTileSize);
  font-size: 14px;
}

.footerTile .catTileName {
  padding: var(--space1);
}

.socialMediaLinksBlock {
  box-sizing: border-box;
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: var(--space1);
  width: fit-content;
  max-width: calc(100% - var(--contentGutter) * 2);
  margin: var(--space2) auto;
  padding: var(--space2);
  border-radius: var(--radiusMd);
  background-color: var(--surfaceCard);
}

.socialmediaicon {
  display: block;
  width: 50px;
  height: auto;
}

/* The bookmark and contact boxes, which were four inline style attributes
   differing only in whether they had a border. */
.footerNote {
  box-sizing: border-box;
  width: min(280px, 100% - var(--contentGutter) * 2);
  margin: var(--space2) auto;
  padding: var(--space1);
  border-radius: var(--radiusMd);
  background-color: var(--surfaceCard);
  text-align: center;
}

.footerNoteBoxed {
  border: 1px solid var(--textBody);
}


/* MEDIA QUERIES
Ordered widest to narrowest. A rule in a later block always wins over the same rule in an earlier one.

The bounds use range syntax — (width < 1200px) — rather than (max-width: 1199.98px).

Both keep the bound exclusive so a viewport of exactly 1200px stays on the desktop base, but the fractional form leaves a dead zone: a 1199.99px viewport (reachable via browser zoom or a fractional device pixel ratio) matches neither. Range syntax has no gap.
Requires Chrome 104+ / Safari 16.4+ / Firefox 104+.

The header needs far fewer of these than it used to: because the layout wraps on
its own, these blocks only step down type and padding rather than restating a
pixel width per breakpoint.
*/

/* Small desktop / large tablet. */
@media (width < 1200px) {

}

/* Tablet, portrait tablet, landscape phone. */
@media (width < 992px) {

  /* .bodyCont drops from 940px to 634px just below here, so three homepage
     cards would be about 198px each. Two keeps them near the 300px they get on
     a tablet today. */
  :root {
    --cardColumns: 2;
  }

}

/* Large phone / small tablet. Sized for 576px, the bottom of this block's range. */
@media (width < 768px) {

  /* Below this the two tracks cannot share a row without squeezing the nav
     labels, so give the nav its own full-width line under the brand. */
  .siteHeaderNav {
    flex-basis: 100%;
  }

}

@media (width < 600px) {

}

/* Phone. */
@media (width < 576px) {

  /* One tile per row. Height follows the content instead of the 350px the wider
     layouts use to keep neighbouring tiles aligned, and the thumbnail loses its
     320px cap so the banner fills the now full-width tile. */
  :root {
    --cardColumns: 1;
    --homeTileMinHeight: 0;
    --mediaFrameMaxWidth: 100%;

    /* Three footer tiles across a 320px phone rather than two. The old fixed
       207px .thumbFooterCont was 1px too narrow for a second 104px tile, so a
       twelve-category footer was twelve rows tall with a dead grey column
       beside it. */
    --footerTileSize: 88px;

    /* Two network cards across a 320px phone. Eight cards one-per-row with a
       visible banner would make the network strip taller than the page above
       it; at 136px they fit two abreast and the strip is shorter than the old
       one-column layout was with the banner clipped to a 64px sliver. */
    --netTileSize: 136px;
  }

  .netFooterTileTitle {
    font-size: 16px;
  }

  /* A 50px icon plus the button's padding is a third of a 320px phone before
     the label starts, which wraps two-word labels onto two lines. */
  .iconLinkIcon {
    width: 32px;
  }

  /* Nothing left to sit beside, so stop floating and centre instead. */
  .allBlocksHome,
  .allThumbTiles {
    float: none;
    margin-inline: auto;
  }

  /* Previous and next stack rather than sharing a row. */
  .prevNextItem {
    flex-basis: 100%;
  }

  .siteHeaderCont {
    gap: var(--space1);
    padding: var(--space1) var(--space2);
  }

  /* One step down each, so the longest site name in use
     ("JimMorrisonTheDoors.com", 23 characters) fits one line at 320px with room
     to spare. Lower these two numbers if a longer name is ever added. */
  .siteHeaderTitle {
    font-size: 18px;
  }

  .siteHeaderTitleLong {
    font-size: 16px;
  }

  .siteDesc {
    font-size: 13px;
  }

  /* Tighter padding buys the width the nav needs to keep its links on one row. */
  .navPill {
    padding-inline: var(--space1);
    font-size: 14px;
  }

}

/* HEIGHT QUERIES
Last in the file, so these override the width blocks above whenever both axes match.

Height is the binding constraint the width blocks cannot see. A landscape phone reports roughly 844x390: wide enough that only the widest width block applies, but far too short for the layout the desktop base hands it.

If you add a height cap here, use dvh rather than vh. Mobile browsers resolve vh against
the expanded viewport, and on a 390px-tall screen the collapsing URL bar is a large
enough share of that to overflow.
*/

/* Landscape phone, split-screen tablet, short desktop window. */
@media (height < 600px) {

  /* Vertical space is the scarce resource here, not horizontal. Flatten the
     header and drop the tagline so content starts above the fold. */
  .siteHeaderCont {
    margin-block: 0;
    padding: var(--space1) var(--space2);
  }

  .siteDesc {
    display: none;
  }

}

/* CAPABILITY QUERIES
After the dimension blocks, because these describe the input device rather than
the viewport and should win regardless of size.
*/

/* Touch and stylus. Pointer precision is what decides hit-target size — a phone
   and a 1080p touchscreen kiosk need the same 44px, and neither is identifiable
   by width alone. */
@media (pointer: coarse) {

  .navPill,
  .adminSiteHeaderLinkCont {
    min-height: var(--tapTargetMin);
  }

  .siteHeaderAdminLink {
    padding-block: var(--space2);
  }

}

@media (prefers-reduced-motion: reduce) {

  .navPill,
  .adminSiteHeaderLinkCont {
    transition: none;
  }

}
