/* Almanac — the design language behind aldenblog.io, applied to the camp so the
   two sites read as the same person's. Blue-grey stock on the page, one hairline
   instead of tinted borders and lifts, three typefaces with one job each, and
   three accents that say what a card is rather than what colour it likes.

   The tokens below are Almanac's own, trimmed to what a shelf of cards needs.
   Light is the default; dark is a designed palette, not an inversion. */

/* ---- fonts ----------------------------------------------------------------

   Three families, one job each: Space Grotesk for the wordmark and card titles,
   Plex Sans for the lede and card copy, Plex Mono for anything that is a fact
   about an app. The blog keeps its serifs; the camp is the sibling that went
   sans, so the shared faces are the two Plex ones. All three are self-hosted —
   the hub has no third-party runtime dependencies and should not gain one for a
   typeface — and all three are SIL OFL 1.1, whose text ships beside them in
   fonts/OFL.txt. Only the weights the page actually sets are shipped. */

@font-face {
  font-family: "Space Grotesk";
  font-style: normal;
  font-weight: 400 700;
  font-display: swap;
  src: url("fonts/space-grotesk-var.woff2") format("woff2");
}

@font-face {
  font-family: "IBM Plex Sans";
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url("fonts/ibm-plex-sans-400.woff2") format("woff2");
}

@font-face {
  font-family: "IBM Plex Mono";
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url("fonts/ibm-plex-mono-400.woff2") format("woff2");
}

@font-face {
  font-family: "IBM Plex Mono";
  font-style: normal;
  font-weight: 500;
  font-display: swap;
  src: url("fonts/ibm-plex-mono-500.woff2") format("woff2");
}

/* ---- tokens ---- */

:root {
  color-scheme: light dark;

  /* surfaces */
  --paper: #e4eaee;
  --surface: #f8fafc;
  --surface-sunk: #d9e2e8;
  --hairline: #c9d4db;

  /* ink. --ink-3 is darker than the original handoff so the 11px mono meta it
     colours clears 4.5:1 on --surface. */
  --ink: #0f1c26;
  --ink-2: #3d515e;
  --ink-3: #5b6f7b;

  /* Three accents, one per kind of thing a card can be: hosted here is post
     green, offsite is project blue, repo-only is link ochre. Post and link
     outline themselves a shade lighter than they set type, since a 1px rule is
     exempt from text contrast; project's blue is one value for both. */
  --accent-post: #1f6155;
  --accent-project: #23508f;
  --accent-link: #8a5327;
  --accent-post-border: #2a6f62;
  --accent-project-border: #23508f;
  --accent-link-border: #9a6234;

  --shadow-card-highlight: none;

  /* Screenshots are other people's colour, dropped onto our stock. A touch of
     desaturation stops a card's contents shouting over the shelf around it. */
  --shot-filter: saturate(.85);

  --font-display: "Space Grotesk", "IBM Plex Sans", sans-serif;
  --font-read: "IBM Plex Sans", system-ui, sans-serif;
  --font-ui: "IBM Plex Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
  --font-mono: "IBM Plex Mono", "SF Mono", Monaco, Consolas, monospace;

  --type-page-title: 600 44px/1.06 var(--font-display);
  --type-card-title: 600 21px/1.22 var(--font-display);
  --type-dek-sm: 400 19px/1.5 var(--font-read);
  --type-card-body: 400 15px/1.6 var(--font-ui);
  --type-meta: 400 11px/1 var(--font-mono);
  --type-badge: 500 10px/1 var(--font-mono);

  --track-page-title: -0.02em;
  --track-meta: .06em;
  --track-chip: .08em;
  --track-badge: .12em;

  --radius-card: 6px;
  --radius-chip: 3px;
  --radius-tag: 2px;

  /* One motion token, applied to border-color and color only. */
  --ease: 180ms ease;
}

/* The floor sits at L~11%, cards about 9% above it, and elevation becomes a
   hairline plus a 4% inset top highlight. Never a drop shadow on a dark
   surface. */
@media (prefers-color-scheme: dark) {
  :root {
    --paper: #101820;
    --surface: #18222b;
    --surface-sunk: #1e2a34;
    --hairline: #2a3742;

    --ink: #e3eaef;
    --ink-2: #a9b8c2;
    --ink-3: #7f909b;

    --accent-post: #8cc4b2;
    --accent-project: #7cb0e0;
    --accent-link: #cfa26a;
    --accent-post-border: #33645a;
    --accent-project-border: #2f5680;
    --accent-link-border: #6a5535;

    --shadow-card-highlight: inset 0 1px 0 rgba(255, 255, 255, .04);

    /* A screenshot is lit for its own page, not for this floor. Taking the
       brightest whites down a notch keeps a card from glowing against it. */
    --shot-filter: brightness(.92) saturate(.85);
  }
}

/* ---- reset ---- */

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

html { -webkit-text-size-adjust: 100%; }

body {
  margin: 0;
  min-height: 100vh;
  padding: clamp(40px, 5vw, 72px) clamp(20px, 4vw, 40px) 48px;
  background: var(--paper);
  color: var(--ink);
  font: var(--type-card-body);
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}

img, picture { max-width: 100%; }

h1, h2 { margin: 0; font-family: var(--font-display); font-weight: 600; color: var(--ink); }

p { margin: 0; }

a { color: var(--accent-project); text-decoration: none; }
a:hover { text-decoration: underline; text-underline-offset: 3px; }

/* Every interactive element gets the same visible ring. */
:focus-visible { outline: 2px solid var(--accent-project); outline-offset: 2px; border-radius: 2px; }

.wrap { width: min(1216px, 100%); margin: 0 auto; }

/* ---- masthead ---- */

/* Wordmark left, everything it has to say bottom-aligned beside it, so the
   small text sits on the wordmark's baseline. Wraps to a stack when there is
   no room. */
header {
  margin-bottom: clamp(32px, 4vw, 44px);
  display: flex;
  align-items: flex-end;
  flex-wrap: wrap;
  gap: clamp(24px, 4vw, 48px);
}

.mark {
  display: inline-flex;
  align-items: baseline;
  font: 600 clamp(56px, 9vw, 88px)/1 var(--font-display);
  letter-spacing: -.03em;
}

.mark em { font-style: normal; color: var(--ink); }

/* "camp" is a window onto the ridgeline painting the blog wears as its
   masthead: the panorama is a background image clipped to the letterforms,
   scaled tall and cropped near the peaks so the ridges land inside the
   x-height. A gradient over it keeps the strokes dark enough to read as type
   first and picture second.

   The letterforms are only ~209px across, so the painting ships at 320w and
   640w. The plain url() is the fallback an engine without image-set() — or
   without its type() argument — falls back to; anything modern takes the AVIF
   at the density it actually needs, which is a twentieth of the full painting. */
.camp {
  font-style: normal;
  color: var(--ink-3);
  padding-bottom: .2em;
  margin-bottom: -.2em;
  background-image:
    linear-gradient(rgba(15, 28, 38, .68), rgba(15, 28, 38, .42) 55%, rgba(15, 28, 38, .16)),
    url("ridge.640.jpg");
  background-image:
    linear-gradient(rgba(15, 28, 38, .68), rgba(15, 28, 38, .42) 55%, rgba(15, 28, 38, .16)),
    image-set(url("ridge.320.avif") type("image/avif") 1x,
              url("ridge.640.avif") type("image/avif") 2x,
              url("ridge.320.jpg") type("image/jpeg") 1x,
              url("ridge.640.jpg") type("image/jpeg") 2x);
  background-size: 100% 370%;
  background-position: center 47%;
  background-repeat: no-repeat;
}

@media (prefers-color-scheme: dark) {
  .camp {
    background-image:
      linear-gradient(rgba(227, 234, 239, .32), rgba(227, 234, 239, .48) 55%, rgba(227, 234, 239, .6)),
      url("ridge-dark.640.jpg");
    background-image:
      linear-gradient(rgba(227, 234, 239, .32), rgba(227, 234, 239, .48) 55%, rgba(227, 234, 239, .6)),
      image-set(url("ridge-dark.320.avif") type("image/avif") 1x,
                url("ridge-dark.640.avif") type("image/avif") 2x,
                url("ridge-dark.320.jpg") type("image/jpeg") 1x,
                url("ridge-dark.640.jpg") type("image/jpeg") 2x);
  }
}

/* Only disappear the letterforms where the painting can actually show through.
   Unguarded, a blocked image or an engine without background-clip erases half
   the site name. */
@supports ((background-clip: text) or (-webkit-background-clip: text)) {
  .camp {
    -webkit-background-clip: text;
    background-clip: text;
    color: transparent;
  }
}

/* Forced colours throw the painting away, so take the letterforms back. */
@media (forced-colors: active) {
  .camp {
    background-image: none;
    color: CanvasText;
  }
}

.says { padding-bottom: 10px; }

.lede { max-width: 24rem; font: var(--type-dek-sm); color: var(--ink-2); }

.by {
  margin-top: 10px;
  font: var(--type-meta);
  letter-spacing: var(--track-meta);
  text-transform: uppercase;
  color: var(--ink-3);
}

/* ---- the shelf ---- */

.grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(19rem, 1fr));
  gap: 20px;
  align-items: stretch;
}

/* Surface, hairline, 6px. That is the entire card treatment: no coloured left
   border, no lift, no glow. */
.card {
  position: relative;
  display: flex;
  flex-direction: column;
  background: var(--surface);
  border: 1px solid var(--hairline);
  border-radius: var(--radius-card);
  box-shadow: var(--shadow-card-highlight);
  overflow: hidden;
  transition: border-color var(--ease);
}

/* Hover moves the border to --ink-3 and nothing else. */
.card:hover,
.card:focus-within { border-color: var(--ink-3); }

.card a { color: inherit; }
.card a:hover { text-decoration: none; }

.shot {
  aspect-ratio: 16 / 10;
  display: grid;
  place-items: center;
  overflow: hidden;
  border-bottom: 1px solid var(--hairline);
  background: var(--surface-sunk);
}

.shot img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: top center;
  display: block;
  filter: var(--shot-filter);
}

/* No screenshot yet: the app's first two letters, set in the title face. */
.shot .glyph {
  font: 600 64px/1 var(--font-display);
  letter-spacing: -.03em;
  color: var(--ink-3);
}

.body {
  padding: 18px 20px 16px;
  display: flex;
  flex-direction: column;
  gap: 8px;
  flex: 1;
}

/* Title and type badge share a baseline, so the badge reads as a label on the
   title rather than as another line of content. */
.hrow {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 12px;
}

.body h2 { font: var(--type-card-title); }

/* The whole card is one link. The ring therefore belongs on the card rather
   than around the inline title text — but only where :has() can put it there,
   so an engine without it still shows a ring of some kind. */
.body h2 a::after {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 1;
}

@supports selector(:has(*)) {
  .body h2 a:focus-visible { outline: none; }

  .card:has(.body h2 a:focus-visible) {
    outline: 2px solid var(--accent-project);
    outline-offset: 2px;
  }
}

.card:hover h2 a { color: var(--accent-project); }

/* What the card is: hosted here, somebody else's host, or only a repo. */
.badge {
  flex: none;
  display: inline-block;
  padding: 3px 7px;
  border: 1px solid;
  border-radius: var(--radius-tag);
  font: var(--type-badge);
  letter-spacing: var(--track-badge);
  text-transform: uppercase;
  white-space: nowrap;
}

.badge--post { color: var(--accent-post); border-color: var(--accent-post-border); }
.badge--project { color: var(--accent-project); border-color: var(--accent-project-border); }
.badge--link { color: var(--accent-link); border-color: var(--accent-link-border); }

.tagline { color: var(--ink-2); }

/* The one icon in the system: it follows the last word of a title that opens
   somebody else's address. Sized in em so it tracks the title. */
.external-link-icon {
  width: .72em;
  height: .72em;
  fill: none;
  stroke: currentColor;
  stroke-width: 2;
  stroke-linecap: round;
  stroke-linejoin: round;
  vertical-align: baseline;
  opacity: .7;
}

/* app.tags is deliberately not rendered: filled chips read as filters, and
   there is nothing to filter yet. The field stays in the registry for when the
   shelf is full enough that browsing by tag earns its place. */

.meta {
  margin-top: auto;
  padding-top: 14px;
  border-top: 1px solid var(--hairline);
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 10px;
  font: var(--type-meta);
  letter-spacing: var(--track-meta);
  text-transform: uppercase;
  color: var(--ink-3);
}

/* Where the card actually takes you — a subdomain here, or somebody else's host. */
.where { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

.who { flex: none; display: flex; gap: 6px; }

/* Meta links sit above the stretched title link so they stay clickable. */
.meta a { position: relative; z-index: 2; color: var(--ink-3); }
.meta a:hover { color: var(--ink-2); text-decoration: underline; text-underline-offset: 3px; }

/* ---- park something here ---- */

/* The invitation is a card that has not been filled in yet, so it is drawn the
   way an empty slot would be: same shape, dashed hairline, no surface. */
.park {
  display: flex;
  flex-direction: column;
  justify-content: center;
  gap: 10px;
  border: 1px dashed var(--hairline);
  border-radius: var(--radius-card);
  padding: 22px 24px;
}

.park h2 { font: var(--type-card-title); }
.park p { color: var(--ink-2); }

.park .go {
  font: var(--type-meta);
  letter-spacing: var(--track-chip);
  text-transform: uppercase;
}

.empty {
  grid-column: 1 / -1;
  border: 1px dashed var(--hairline);
  border-radius: var(--radius-card);
  padding: 48px 16px;
  text-align: center;
  color: var(--ink-2);
}

/* ---- footer ---- */

footer {
  margin-top: clamp(32px, 4vw, 44px);
  padding-top: 22px;
  border-top: 1px solid var(--hairline);
  display: flex;
  flex-wrap: wrap;
  gap: 12px 24px;
  font: var(--type-meta);
  letter-spacing: var(--track-meta);
  text-transform: uppercase;
  color: var(--ink-3);
}

footer a { color: var(--ink-3); }
footer a:hover { color: var(--ink-2); }
footer .right { margin-left: auto; }

/* ---- 404 ---- */

.oops { width: min(38rem, 100%); margin: 12vh auto 0; text-align: center; }
.oops h1 { font: var(--type-page-title); letter-spacing: var(--track-page-title); margin-bottom: 14px; }
.oops p { font: var(--type-dek-sm); color: var(--ink-2); }
.oops code { font-family: var(--font-mono); font-size: .9em; color: var(--ink); }

/* Nothing in this system is fully rounded, so the way back is a chip. */
.oops .cta {
  display: inline-block;
  margin-top: 26px;
  padding: 9px 14px;
  border: 1px solid var(--hairline);
  border-radius: var(--radius-chip);
  font: var(--type-meta);
  letter-spacing: var(--track-chip);
  text-transform: uppercase;
  color: var(--ink-2);
  transition: border-color var(--ease), color var(--ease);
}

.oops .cta:hover { border-color: var(--ink-3); color: var(--ink); text-decoration: none; }

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: .01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: .01ms !important;
    scroll-behavior: auto !important;
  }
}
