/* 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)
*/


/* 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;

  /* 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;

  /* Shadows */
  --shadowCard: 4px 4px #a9a9a9;

  /* 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;

  /* 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;
}

/* 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 {
  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;
}


/* 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%;
  }

  /* 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;
  }

}
