/* bollenbach.ca
   One stylesheet. No build step, no framework, no global JavaScript.
   Light mode only.

   Two scales, and nothing off them:
     type     --display 30  --title 22  --lede 19  --body 17  --small 15 (px)
     spacing  --s1 .25  --s2 .5  --s3 1  --s4 2  --s5 3  --s6 5  (rem)

   Font stacks are hardcoded, not read from --gh-font-*, so that picking a
   font in Ghost Admin cannot pull a webfont in and break the serif/sans
   contrast the hierarchy is built on. This costs one gscan warning
   ("Missing support for custom fonts"), which is accepted deliberately. */

:root {
  color-scheme: light;

  --bg: #fff;
  --text: #16181a;
  /* Secondary text carries a lot of real content here (descriptions, excerpts,
     dates), so it sits at ~7.5:1 rather than skimming the 4.5:1 AA floor.
     Still clearly recessive against --text at 17.8:1. */
  --muted: #4a4f55;
  --rule: #e4e5e7;
  --accent: var(--ghost-accent-color, #3d6b45);

  --serif: ui-serif, Georgia, "Times New Roman", serif;
  --sans: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
  --mono: ui-monospace, SFMono-Regular, Menlo, monospace;

  /* Five type roles, named for their job. Never write a raw font-size:
     if a new element needs type, it takes one of these or it is not a
     new role. Leading tightens as size grows; tracking follows suit,
     because a 30px serif needs pulling in and 15px sans needs opening up. */
  --display: 1.875rem;   /* 30px  page and post titles          */
  --title:   1.375rem;   /* 22px  section headings in prose     */
  --lede:    1.1875rem;  /* 19px  standfirst, subheads          */
  --body:    1.0625rem;  /* 17px  prose                         */
  --small:   0.9375rem;  /* 15px  metadata, captions, secondary */

  --lh-display: 1.25;
  --lh-title:   1.35;
  --lh-lede:    1.5;
  --lh-body:    1.65;
  --lh-small:   1.45;

  --track-display: -0.015em;
  --track-title:   -0.01em;
  --track-small:    0.005em;

  --s1: 0.25rem;
  --s2: 0.5rem;
  --s3: 1rem;
  --s4: 2rem;
  --s5: 3rem;
  --s6: 5rem;

  /* ~68 characters of prose at --body. Wider than this and the eye has to
     travel too far back to find the next line. */
  --width: 36rem;
  --wide: 56rem;
  --pad: 1.25rem;
}

/* Base ------------------------------------------------------------------- */

*, *::before, *::after { box-sizing: border-box; }

body {
  max-width: var(--width);
  margin: 0 auto;
  padding: var(--s5) var(--pad) var(--s6);
  background: var(--bg);
  color: var(--text);
  font-family: var(--sans);
  font-size: var(--body);
  line-height: var(--lh-body);
  overflow-wrap: break-word;
  -webkit-text-size-adjust: 100%;

  /* Opening quotes and punctuation hang into the margin so the left edge of
     the text stays optically straight. Safari only for now; free everywhere
     else. */
  hanging-punctuation: first last;

  /* Rendering. System UI faces are drawn for labels, not paragraphs, so they
     come out heavier than they need to be at reading sizes. Greyscale
     antialiasing thins the strokes and is most of what reads as "smooth".
     Kerning and ligatures are off by default in some engines below 20px. */
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  font-kerning: normal;
  font-variant-ligatures: common-ligatures contextual;
  font-optical-sizing: auto;
}

/* Softens the ragged right edge without justifying, which would open rivers
   at this measure. The limits stop it hyphenating short words. */
.intro p,
.post-content p,
.post-content li {
  hyphens: auto;
  hyphenate-limit-chars: 9 4 4;
}

h1, h2, h3, h4, h5, h6 {
  font-family: var(--serif);
  font-weight: 400;
  text-wrap: balance;
  margin: var(--s4) 0 var(--s2);
}

h1 { font-size: var(--display); line-height: var(--lh-display); letter-spacing: var(--track-display); margin-top: 0; }

/* Page title and its subtitle are one unit: tight together, big gap below.
   Without the :has(), the subtitle sits closer to the first item on the page
   than to the title it belongs to. Post and home h1s are nested, so they are
   unaffected by both rules. */
main > h1 { margin-bottom: var(--s4); }
main > h1:has(+ .post-lede) { margin-bottom: var(--s1); }
main > .post-lede { margin-bottom: var(--s5); }
h2 { font-size: var(--title); line-height: var(--lh-title); letter-spacing: var(--track-title); }
h3 { font-size: var(--lede); line-height: var(--lh-lede); }

/* h4-h6 are rare and sit at body size, so weight carries the rank instead
   of a sixth size nobody would recognise. */
h4, h5, h6 { font-size: var(--body); line-height: var(--lh-lede); font-weight: 500; }

p { text-wrap: pretty; }
p, ul, ol, figure, blockquote, pre, table { margin: 0 0 var(--s3); }
ul, ol { padding-left: var(--s3); }

/* Underlines are for prose, where you genuinely cannot tell what is a link.
   Everywhere else the link stays plain and reveals itself on hover.

   The underline is always drawn but transparent, so revealing it is a colour
   change rather than a layout change: nothing shifts, and it can ease. */
a {
  color: inherit;
  text-decoration: underline;
  text-decoration-color: transparent;
  text-underline-offset: 0.18em;
  text-decoration-thickness: 1px;
  transition: text-decoration-color 140ms ease;
}

a:hover { text-decoration-color: var(--accent); }
a:visited { color: inherit; }

/* In prose the underline is visible at rest, and warms to the accent on hover. */
.gh-content a { text-decoration-color: currentColor; }
.gh-content a:hover { text-decoration-color: var(--accent); }

img, video { max-width: 100%; height: auto; display: block; }
iframe { max-width: 100%; }

/* The small role, declared once. Anything secondary joins this list rather
   than restating the size, which is what let ten selectors drift apart. */
small,
.meta,
.link-desc,
.pagination,
.post-footer,
.post-item p,
figcaption,
footer,
.kg-bookmark-description,
.kg-bookmark-metadata {
  font-size: var(--small);
  line-height: var(--lh-small);
  letter-spacing: var(--track-small);
}

/* The lede role, likewise. The intro bio deliberately does NOT take it: it is
   several paragraphs of the same kind of text, so a larger first paragraph
   reads as the type shrinking mid-bio rather than as hierarchy. A lede is one
   standfirst sentence, which is what .post-lede is. */
.post-lede {
  font-size: var(--lede);
  line-height: var(--lh-lede);
}

small, .meta { color: var(--muted); }
time { font-variant-numeric: tabular-nums; }

code, pre { font-family: var(--mono); font-size: var(--small); }
pre { padding-left: var(--s3); border-left: 1px solid var(--rule); white-space: pre-wrap; }
pre code { font-size: inherit; }

blockquote {
  margin-left: 0;
  padding-left: var(--s3);
  border-left: 1px solid var(--rule);
  color: var(--muted);
}

table { width: 100%; border-collapse: collapse; }
th, td { text-align: left; padding: var(--s2) var(--s3) var(--s2) 0; overflow-wrap: anywhere; }

/* A rule is space, not a line. */
hr { border: 0; height: 0; margin: var(--s6) 0; }

:focus-visible { outline: 2px solid var(--text); outline-offset: 2px; }

/* The one place the accent appears without being asked for. */
::selection { background: color-mix(in srgb, var(--accent) 18%, transparent); color: var(--text); }

@media (prefers-contrast: more) {
  :root { --muted: #33373c; --rule: #b9bcbf; }
  :focus-visible { outline-width: 3px; }
}

/* Breakout: the only way anything exceeds the reading measure. */
.wide {
  width: min(var(--wide), calc(100vw - 2 * var(--pad)));
  max-width: none;
  margin-left: 50%;
  transform: translateX(-50%);
}

/* Header and footer ------------------------------------------------------- */

header { margin-bottom: var(--s5); }

/* The header is one typeface, one size. Only colour separates identity
   from navigation. */
.wordmark { margin-right: var(--s4); }

header nav { display: inline; }

header nav a {
  color: var(--muted);
  margin-right: var(--s3);
  padding-block: var(--s1);
}

footer {
  margin-top: var(--s6);
  color: var(--muted);
}

footer nav a { margin-right: var(--s3); padding-block: var(--s1); }
footer nav a:last-child { margin-right: 0; }
footer p { margin: var(--s3) 0 0; }

/* Sits in a row of footer links, so it behaves like one. */
.subscribe {
  background: none;
  border: 0;
  padding: 0;
  font: inherit;
  /* Form controls get letter-spacing: normal from the UA sheet, which would
     drop this out of the small role it sits in. */
  letter-spacing: inherit;
  color: inherit;
  text-decoration: none;
  text-underline-offset: 0.18em;
  cursor: pointer;
}

.subscribe:hover { text-decoration: underline; }

/* Home -------------------------------------------------------------------- */

.intro { margin-bottom: var(--s5); }

/* The bio is prose, so links in it are underlined at rest like prose links,
   even though the intro is not inside .gh-content. */
.intro a { text-decoration-color: currentColor; }
.intro a:hover { text-decoration-color: var(--accent); }

/* The headline must sit further from the bio than the bio's own paragraphs sit
   from each other. At the shared 8px heading margin it bound tighter to the
   first paragraph than the paragraphs did to one another, so the serif
   headline read as the opening line of the sans body copy. */
.intro h1 { margin-bottom: var(--s4); }
.intro p { margin-bottom: var(--s3); }
.intro p:last-child { margin-bottom: 0; }

/* These are real h2s and they look like it: serif, at heading size, like every
   other heading on the site. Earlier attempts styled them as small sans text,
   which made them read first as metadata and then as list items, because
   nothing distinguished them from the rows underneath. The class now only
   carries spacing. */
.section-label { margin: var(--s5) 0 var(--s3); }

.link-list, .post-list { list-style: none; padding: 0; }
.link-list li { margin-bottom: var(--s2); }

/* Stacked on narrow screens, aligned into two columns on wide ones. The name
   and the description need a structural boundary: with both on one line,
   separated only by a space, the row reads as one sentence that changes
   colour and size halfway through. */
.link-desc { display: block; color: var(--muted); }

@media (min-width: 34rem) {
  .link-list li {
    display: grid;
    grid-template-columns: 13rem 1fr;
    gap: var(--s3);
    margin-bottom: var(--s2);
  }
}

/* Post lists -------------------------------------------------------------- */

/* Rows need to be further apart than the parts inside a row, or a title, a
   three-line excerpt and a date all merge into one block of text. */
.post-list li { margin-bottom: var(--s4); }

.post-item {
  display: flex;
  gap: var(--s3);
  align-items: flex-start;
  text-decoration: none;
}

/* Keep one left axis even when a post has no feature image. */
.post-item:not(:has(img))::before { content: ""; flex: 0 0 4rem; }

.post-item img { flex: 0 0 4rem; width: 4rem; height: 4rem; object-fit: cover; }
.post-item-body { min-width: 0; }

.post-item { text-decoration: none; }
.post-item h3 {
  margin: 0 0 var(--s2);
  text-decoration: underline;
  text-decoration-color: transparent;
  text-underline-offset: 0.18em;
  text-decoration-thickness: 1px;
  transition: text-decoration-color 140ms ease;
}
.post-item:hover h3 { text-decoration-color: var(--accent); }
.post-item img { transition: opacity 140ms ease; }
.post-item:hover img { opacity: 0.85; }
/* Clamp to two lines with a real ellipsis. Ghost's {{excerpt words=n}} cuts at
   an arbitrary word and adds nothing, so excerpts ended mid-phrase and every
   row was a different height. The template asks for more words than fit and
   lets the clamp do the trimming. */
.post-item p {
  margin: 0 0 var(--s2);
  color: var(--muted);
  display: -webkit-box;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 2;
  line-clamp: 2;
  overflow: hidden;
}
.post-item .meta { display: block; }

.more { margin-top: var(--s4); }

.pagination {
  display: flex;
  justify-content: space-between;
  gap: var(--s3);
  margin-top: var(--s5);
  color: var(--muted);
}

/* Projects ---------------------------------------------------------------- */

/* Name and category are a single label, so no gap between them. One gap
   inside the block, a much larger one between blocks. */
.project { margin-bottom: var(--s5); }
.project h2 { margin: 0; }
.project .meta { margin: 0 0 var(--s2); }
.project p:last-child { margin-bottom: 0; }

/* Post -------------------------------------------------------------------- */

.post-header { margin-bottom: var(--s4); }
.post-header .meta { display: block; margin-top: var(--s3); }
.post-lede { color: var(--muted); }
.post-header .post-lede { margin-bottom: 0; }

/* Text obeys the reading measure. Images have no reason to. The feature image
   breaks out to --wide, which is what the 60% of screen sitting empty either
   side of the column is for. */
.post-feature {
  width: min(var(--wide), calc(100vw - 2 * var(--pad)));
  max-width: none;
  margin: 0 0 var(--s4);
  margin-left: 50%;
  transform: translateX(-50%);
}

.post-feature img { width: 100%; }

/* The caption is text, so it goes back to a text measure and hangs at the
   left edge of the image rather than centring under it. */
.post-feature figcaption { max-width: 32rem; }

figcaption {
  max-width: 32rem;
  margin-top: var(--s2);
  color: var(--muted);
}

.post-content > figure { margin: var(--s4) 0; }

.post-footer { margin-top: var(--s5); color: var(--muted); }

/* Related reads as an afterword, not a second article. */
.related { margin-top: var(--s6); }
.related > .section-label { margin-top: 0; }
.related .post-item img,
.related .post-item::before,
.related .post-item p { display: none; }
.related .post-list li { margin-bottom: var(--s2); }

/* Ghost content cards ----------------------------------------------------- */

.gh-content .kg-width-wide,
.gh-content .kg-width-full {
  width: min(var(--wide), calc(100vw - 2 * var(--pad)));
  max-width: none;
  margin-left: 50%;
  transform: translateX(-50%);
}

.kg-gallery-row { display: flex; gap: var(--s2); margin-bottom: var(--s2); }
.kg-gallery-image img { width: 100%; height: 100%; object-fit: cover; }

/* Ghost's cards.min.css sizes card text in em, which lands on values like
   19.55px and 16.15px inside posts. Pull every card back onto the role scale. */
/* Ghost's selectors are .kg-callout-card div.kg-callout-text (0,2,1) and its
   sheet loads after this one, so these have to carry a class more to win. */
.gh-content .kg-callout-card div.kg-callout-text,
.gh-content .kg-callout-card div.kg-callout-emoji,
.gh-content .kg-toggle-card .kg-toggle-heading-text,
.gh-content .kg-toggle-card .kg-toggle-content,
.gh-content .kg-bookmark-card .kg-bookmark-title {
  font-size: var(--body);
  line-height: var(--lh-body);
  letter-spacing: normal;
}

.gh-content .kg-bookmark-card .kg-bookmark-description,
.gh-content .kg-bookmark-card .kg-bookmark-metadata,
.gh-content .kg-card figcaption {
  font-size: var(--small);
  line-height: var(--lh-small);
}

/* Ghost ships coloured, rounded callouts and bookmark cards. Mute them. */
.gh-content [class*="kg-callout-card"] {
  background: none;
  border: 0;
  border-left: 1px solid var(--rule);
  border-radius: 0;
  padding: 0 0 0 var(--s3);
}

.kg-bookmark-card a { text-decoration: none; }
.kg-bookmark-title { text-decoration: underline; }
.kg-bookmark-description, .kg-bookmark-metadata { color: var(--muted); }
.kg-bookmark-thumbnail, .kg-bookmark-icon { display: none; }

/* Photography ------------------------------------------------------------- */

.trip-map { height: 20rem; margin-bottom: var(--s4); }

/* Page transitions --------------------------------------------------------

   Cross-document view transitions: the browser crossfades between pages on
   navigation. No JavaScript, no router, no framework. Browsers without support
   simply navigate as before. Naming the header and footer holds them still
   while the content changes, so the chrome does not flicker. */

@view-transition { navigation: auto; }

header { view-transition-name: site-header; }
footer { view-transition-name: site-footer; }

::view-transition-old(root),
::view-transition-new(root) {
  animation-duration: 180ms;
  animation-timing-function: ease;
}

/* Everything above is decoration. Anyone who has asked their OS for less
   motion gets none of it. */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    transition-duration: 0s !important;
    animation-duration: 0s !important;
    animation-iteration-count: 1 !important;
  }
}

@media (prefers-reduced-motion: reduce) {
  @view-transition { navigation: none; }
}

/* Print ------------------------------------------------------------------- */

@media print {
  body { max-width: none; padding: 0; color: #000; }
  header nav, footer, .post-footer, .related, .trip-map { display: none; }
  .wordmark { text-decoration: none; }
  a[href^="http"]::after { content: " (" attr(href) ")"; font-size: var(--small); }
}
