/* =========================================================================
   Tracey Exton — Scottish landscape painter
   One stylesheet for the public site.

   The site is a catalogue. Every painting is a plate: printed to its edges
   at its own proportion, as large as its column allows, with no mount, no
   frame and no shadow. The type is one quiet grotesque for everything the
   site says, and one serif italic for the one thing that is not the site's
   to say -- the name of a work. Nothing has a radius; nothing moves unasked.

   Sections: 1 fonts, 2 tokens, 3 base, 4 type, 5 layout, 6 links and
   buttons, 7 header, then the pages in site order, 27 mobile, 28 utility.
   ========================================================================= */

/* ---------- 1. Fonts ---------- */

/* Self-hosted. Provenance and licence: static/fonts/README.md. Paths stay
   relative so ManifestStaticFilesStorage rewrites them on collect. The
   unicode-range values are the ones Google's CSS served with these files. */

@font-face {
  font-family: "Hanken Grotesk";
  font-style: normal;
  font-weight: 300 800;
  font-display: swap;
  src: url("../fonts/hankengrotesk-normal-latin.c501f578bf3f.woff2") format("woff2");
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA,
    U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191,
    U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

@font-face {
  font-family: "Hanken Grotesk";
  font-style: normal;
  font-weight: 300 800;
  font-display: swap;
  src: url("../fonts/hankengrotesk-normal-latin-ext.20672d217485.woff2") format("woff2");
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7,
    U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF,
    U+2020, U+20A0-20AB, U+20AD-20C4, U+2113, U+2C60-2C7F, U+A720-A7FF;
}

/* Newsreader at its 24pt optical size, one weight: it only ever sets a
   painting's title, and in the studio manager the artist's own words. */
@font-face {
  font-family: "Newsreader";
  font-style: italic;
  font-weight: 400;
  font-display: swap;
  src: url("../fonts/newsreader-italic-latin.c00b819a6d88.woff2") format("woff2");
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA,
    U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191,
    U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

@font-face {
  font-family: "Newsreader";
  font-style: italic;
  font-weight: 400;
  font-display: swap;
  src: url("../fonts/newsreader-italic-latin-ext.755a6bc905ec.woff2") format("woff2");
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7,
    U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF,
    U+2020, U+20A0-20AB, U+20AD-20C4, U+2113, U+2C60-2C7F, U+A720-A7FF;
}

@font-face {
  font-family: "Newsreader";
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url("../fonts/newsreader-normal-latin.a82389636424.woff2") format("woff2");
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA,
    U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191,
    U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

@font-face {
  font-family: "Newsreader";
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url("../fonts/newsreader-normal-latin-ext.d964212c0654.woff2") format("woff2");
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7,
    U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF,
    U+2020, U+20A0-20AB, U+20AD-20C4, U+2113, U+2C60-2C7F, U+A720-A7FF;
}

/* ---------- 2. Tokens ---------- */

:root {
  /* Ink and paper as channels, so an opacity step is written inline as
     rgb(var(--ink-rgb) / .5) rather than earning a token. */
  --ink-rgb: 35 35 35;
  --paper-rgb: 249 249 247;

  --paper: rgb(var(--paper-rgb));
  /* A step down from the paper, for tinted bands and form fields. The name
     is kept from the hung-wall design so the studio manager's stylesheet,
     which shares these tokens, needs no change. */
  --mount: #efefec;
  --mount-deep: #e6e6e2;
  --ink: rgb(var(--ink-rgb));

  /* One colour for every piece of secondary text: 5.1:1 on the paper and
     4.6:1 on the tinted band, so it can be used at the caption size. */
  --text-soft: #6b6b68;

  --on-dark: rgb(var(--paper-rgb));
  --on-dark-soft: rgb(var(--paper-rgb) / 0.75);
  --on-dark-faint: rgb(var(--paper-rgb) / 0.6);

  /* Slate: the one accent, on links and the focus ring. Cool, so it sits
     beside her blues without matching them. */
  --accent: #4b5d78;

  --rule: 1px solid rgb(var(--ink-rgb) / 0.14);
  --border-secondary: 1px solid rgb(var(--ink-rgb) / 0.3);

  --font-ui: "Hanken Grotesk", "Helvetica Neue", Arial, sans-serif;
  --font-display: "Newsreader", Georgia, "Times New Roman", serif;

  /* The scale: six sizes, base 16, ratio 1.25. Every piece of type on the
     site is one of these. */
  --size-caption: 13px;
  --size-body: 16px;
  --size-lead: 19px;
  --size-title: 23px;
  --size-heading: 28px;
  --size-display: 42px;

  --leading-tight: 1.05;
  --leading-heading: 1.15;
  --leading-title: 1.2;
  --leading-lead: 1.5;
  --leading-body: 1.6;

  /* Wide margins: the plates are large and the paper around them is what
     makes them read as hung in a room rather than packed on a page. */
  --shell-max: 1360px;
  --gutter: 64px;
  --section-pad: 64px;

  /* A section that pads itself caps on the outer measure, so its content
     box is still --shell-max wide; a .shell inside an already-padded section
     caps on --shell-max directly. Mixing the two drifts left edges by a gutter. */
  --shell-outer: calc(var(--shell-max) + 2 * var(--gutter));

  --header-height: 92px;

  --overlay: rgb(var(--ink-rgb) / 0.94);

  /* A plate is printed to its edges, not hung: nothing on the public site
     casts a shadow. The token stays for the studio manager's rack. */
  --shadow-hung: none;

  /* The gallery's red dot, beside the word "Sold" and nowhere else. The one
     state colour the public site carries; the word says it without the dot. */
  --sold-dot: #b23a2e;

  /* The one colour the public site has no use for. It appears on the studio
     manager's delete confirmation and nowhere else. */
  --danger: oklch(0.48 0.13 25);
}

/* ---------- 3. Reset and base ---------- */

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

html {
  /* Anchored sections must clear the sticky header. */
  scroll-padding-top: var(--header-height);
}

@media (prefers-reduced-motion: no-preference) {
  html {
    scroll-behavior: smooth;
  }
}

body {
  margin: 0;
  background: var(--paper);
  color: var(--ink);
  font: 400 var(--size-body) / var(--leading-body) var(--font-ui);
  -webkit-font-smoothing: antialiased;
}

img {
  max-width: 100%;
}

/* A button starts as a blank slate, like every other element here. The site dresses
   links as buttons, so the component classes were written assuming there was nothing
   to undo -- and a real <button> kept the browser's grey box and border underneath.
   Components that want a border or a background still set their own. */
button {
  margin: 0;
  padding: 0;
  font: inherit;
  color: inherit;
  background: none;
  border: 0;
  cursor: pointer;
}

h1,
h2,
h3,
p,
figure {
  margin: 0;
}

/* Both list types, because an ordered list is still a list: the numbering carries
   meaning in the markup without the browser's indent and markers being wanted on
   screen. Anything that does want them says so. */
ul,
ol {
  margin: 0;
  padding: 0;
  list-style: none;
}

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

/* Selected text inverts the page's two colours rather than taking the
   browser's blue; the dark grounds invert back. */
::selection {
  background: var(--ink);
  color: var(--paper);
}

.band ::selection,
.lightbox--dark ::selection {
  background: var(--paper);
  color: var(--ink);
}

/* ---------- 4. Typography ---------- */

/* Headings are the same grotesque as everything else, set large and a
   touch tight. Only a painting's title is ever in the serif. */
.section__title {
  font: 400 var(--size-display) / var(--leading-tight) var(--font-ui);
  letter-spacing: -0.015em;
  text-wrap: balance;
}

.section__title--small {
  font-size: var(--size-heading);
  line-height: var(--leading-heading);
  letter-spacing: -0.01em;
}

.section__intro {
  margin-top: 16px;
  max-width: 34em;
  font: 400 var(--size-lead) / var(--leading-lead) var(--font-ui);
  text-wrap: pretty;
}

.section__meta {
  font: 400 var(--size-caption) / var(--leading-lead) var(--font-ui);
  color: var(--text-soft);
}

/* A line of fact above a heading -- what kind of thing this is, or where it
   was shown. Sentence case, in the accent: it is information, not a label. */
.eyebrow {
  font: 500 var(--size-caption) / var(--leading-lead) var(--font-ui);
  color: var(--accent);
}

.eyebrow--on-dark {
  color: var(--on-dark-soft);
}

/* The name of a work, wherever it appears: the one thing set in the serif. */
.work-title {
  font: italic 400 var(--size-title) / var(--leading-title) var(--font-display);
}

/* Figures line up when they sit in a column or a row. */
.figures {
  font-variant-numeric: tabular-nums;
}

/* ---------- 5. Layout ---------- */

.shell {
  max-width: var(--shell-max);
  margin: 0 auto;
}

.section {
  padding: var(--section-pad) var(--gutter);
}

.section--ruled-top {
  border-top: var(--rule);
}

.section--ruled-bottom {
  border-bottom: var(--rule);
}

.section--tinted {
  background: var(--mount);
}

.section--flush {
  padding: 0;
}

/* The studio collection sits deeper on its base than the sections around it,
   so the grid has room to breathe before the tinted band. */
.section--deep {
  padding-bottom: 96px;
}

.section__head {
  display: flex;
  justify-content: space-between;
  align-items: flex-end;
  gap: 32px;
}

.section__head--baseline {
  align-items: baseline;
}

/* ---------- 6. Links and buttons ---------- */

a {
  color: inherit;
  text-decoration: none;
}

.button-row {
  display: flex;
  flex-wrap: wrap;
  gap: 12px;
  margin-top: 32px;
}

.button {
  display: inline-block;
  font: 500 var(--size-caption) / 1 var(--font-ui);
  padding: 15px 24px;
}

.button--primary {
  background: var(--ink);
  color: var(--paper);
}

.button--primary:hover {
  background: rgb(var(--ink-rgb) / 0.82);
}

.button--secondary {
  padding: 14px 23px;
  border: var(--border-secondary);
}

.button--secondary:hover {
  border-color: var(--ink);
}

/* Waiting on the server: the button stays where it is and says so by receding. */
.button.htmx-request {
  opacity: 0.5;
  cursor: progress;
}

/* A text link is underlined, which is what a link looks like; the line is
   half-strength until the pointer arrives. No glyph follows it. */
.text-link {
  font: 500 var(--size-body) / var(--leading-body) var(--font-ui);
  color: var(--accent);
  text-decoration: underline;
  text-decoration-thickness: 1px;
  text-underline-offset: 4px;
  text-decoration-color: color-mix(in srgb, currentcolor 50%, transparent);
}

.text-link:hover {
  text-decoration-color: currentcolor;
}

.text-link--on-dark {
  display: inline-block;
  align-self: flex-start;
  color: var(--on-dark);
}

/* The arrow in an icon control (templates/partials/_arrow.html). It takes the
   control's font-size, so a control that set its glyph's size still does, and
   the stroke stays one weight whatever size it is drawn at. */
.arrow {
  display: inline-block;
  width: 1em;
  height: 1em;
  vertical-align: middle;
  fill: none;
  stroke: currentcolor;
  stroke-width: 1.5px;
}

.arrow path {
  vector-effect: non-scaling-stroke;
}

/* ---------- 7. Header and menu ---------- */

.skip-link {
  position: absolute;
  left: -9999px;
  top: 0;
  z-index: 20;
  padding: 15px 24px;
  background: var(--ink);
  color: var(--paper);
  font: 500 var(--size-caption) / 1 var(--font-ui);
}

.skip-link:focus {
  left: 0;
}

.site-header {
  position: sticky;
  top: 0;
  z-index: 10;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 32px;
  padding: 24px var(--gutter);
  background: rgb(var(--paper-rgb) / 0.94);
  backdrop-filter: blur(6px);
  border-bottom: var(--rule);
}

.site-header__lockup {
  display: flex;
  flex-direction: column;
}

.site-header__wordmark {
  font: 500 var(--size-lead) / var(--leading-title) var(--font-ui);
}

.site-header__tagline {
  font: 400 var(--size-caption) / var(--leading-lead) var(--font-ui);
  color: var(--text-soft);
}

.site-nav__list {
  display: flex;
  gap: 32px;
  font: 400 var(--size-body) / var(--leading-title) var(--font-ui);
  color: var(--text-soft);
}

.site-nav__link:hover {
  color: var(--ink);
}

.site-nav__link[aria-current="page"] {
  color: var(--ink);
  text-decoration: underline;
  text-decoration-thickness: 1px;
  text-underline-offset: 6px;
}

/* The menu is a <details>, so it opens and closes with no JavaScript;
   CloseMenu adds close-on-tap and Escape on top. */
.menu {
  display: none;
}

.menu__toggle {
  display: flex;
  flex-direction: column;
  justify-content: center;
  gap: 6px;
  width: 44px;
  height: 44px;
  padding: 10px;
  cursor: pointer;
  list-style: none;
}

.menu__toggle::-webkit-details-marker {
  display: none;
}

.menu__bar {
  display: block;
  height: 1px;
  background: var(--ink);
}

.menu__list {
  position: absolute;
  top: 100%;
  left: 0;
  right: 0;
  display: flex;
  flex-direction: column;
  gap: 20px;
  padding: 24px var(--gutter) 32px;
  background: var(--paper);
  border-bottom: var(--rule);
  font: 400 var(--size-lead) / var(--leading-title) var(--font-ui);
}

.menu__link[aria-current="page"] {
  text-decoration: underline;
  text-decoration-thickness: 1px;
  text-underline-offset: 6px;
}

/* ---------- 8. Hero ---------- */

.hero {
  display: grid;
  grid-template-columns: 5fr 7fr;
  gap: 64px;
  align-items: center;
  max-width: var(--shell-outer);
  margin: 0 auto;
  padding: 64px var(--gutter);
}

.hero--unillustrated {
  grid-template-columns: 1fr;
}

.hero__title {
  font: 400 var(--size-display) / var(--leading-tight) var(--font-ui);
  letter-spacing: -0.015em;
  text-wrap: balance;
}

.hero__intro {
  margin-top: 24px;
  max-width: 28em;
  font: 400 var(--size-lead) / var(--leading-lead) var(--font-ui);
  text-wrap: pretty;
}

.hero__figure {
  display: flex;
  flex-direction: column;
  gap: 12px;
}

.hero__mount {
  display: block;
}

/* The width and height attributes reserve the plate's box while it loads;
   auto keeps them from setting the rendered height. */
.hero__image {
  display: block;
  width: 100%;
  height: auto;
}

.hero__caption {
  font: 400 var(--size-caption) / var(--leading-lead) var(--font-ui);
  color: var(--text-soft);
}

.hero__caption-title {
  font: italic 400 var(--size-body) / var(--leading-lead) var(--font-display);
  color: var(--ink);
}

/* ---------- 9. Painting grid ---------- */

/* Three plates to a row, each the full width of its column at its own
   proportion, top-aligned so a row reads as a row and the bottoms fall where
   the paintings' shapes put them. The dimensions in the caption say how big
   a work is; the grid does not try to. */
.painting-grid {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: 64px 48px;
  align-items: start;
  margin-top: 48px;
}

.painting-card {
  position: relative;
  display: flex;
  flex-direction: column;
  gap: 4px;
}

.painting-card__image {
  display: block;
  width: 100%;
  height: auto;
  margin-bottom: 12px;
}

/* A sold plate stays in the catalogue; the colour drains rather than the
   card changing shape, so the grid's rhythm survives a sold-out row. */
.painting-card--sold .painting-card__image {
  filter: saturate(0.45);
}

.painting-card__line {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  gap: 16px;
}

.painting-card__title {
  font: italic 400 var(--size-title) / var(--leading-title) var(--font-display);
}

/* The title is the card's one link; this carries its hit area over the whole
   card, plate included, so the painting opens from anywhere on it without a
   second, hidden link. */
.painting-card__title::after {
  content: "";
  position: absolute;
  inset: 0;
}

.painting-card__title:hover {
  text-decoration: underline;
  text-decoration-thickness: 1px;
  text-underline-offset: 4px;
}

.painting-card__price {
  font: 500 var(--size-caption) / var(--leading-lead) var(--font-ui);
  white-space: nowrap;
}

.painting-card__price--status {
  font-weight: 400;
  color: var(--text-soft);
}

.painting-card__meta {
  min-width: 0;
  font: 400 var(--size-caption) / var(--leading-lead) var(--font-ui);
  color: var(--text-soft);
}

/* Raised above the stretched title link so it can be clicked. */
.painting-card__enquire {
  position: relative;
  z-index: 1;
  font: 500 var(--size-caption) / var(--leading-lead) var(--font-ui);
  color: var(--accent);
  text-decoration: underline;
  text-decoration-thickness: 1px;
  text-underline-offset: 4px;
  text-decoration-color: color-mix(in srgb, currentcolor 50%, transparent);
  white-space: nowrap;
}

.painting-card__enquire:hover {
  text-decoration-color: currentcolor;
}

/* The button is a row of the grid, so the six it brings in take its place and
   the next button follows them without anything having to be rebuilt. */
.grid-paging {
  grid-column: 1 / -1;
}

/* ---------- 10. Collections ---------- */

.collection-grid {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: 48px;
  align-items: start;
  margin-top: 40px;
}

.collection-card {
  display: flex;
  flex-direction: column;
  gap: 4px;
}

/* A cover is a painting too, so it is printed whole at its own proportion. */
.collection-card__image {
  display: block;
  width: 100%;
  height: auto;
  margin-bottom: 12px;
}

.collection-card__placeholder {
  aspect-ratio: 4 / 3;
  background: rgb(var(--ink-rgb) / 0.06);
}

.collection-card__name {
  font: 500 var(--size-lead) / var(--leading-title) var(--font-ui);
}

.collection-card:hover .collection-card__name {
  text-decoration: underline;
  text-decoration-thickness: 1px;
  text-underline-offset: 4px;
}

.collection-card__description {
  color: var(--text-soft);
}

/* ---------- 11. Save Me! band ---------- */

/* The ink runs to both edges, because the page is a sequence of grounds; the
   content inside it sits on the same measure as every other section, so the
   band reads as a change of ground and not a change of page. */
.band {
  padding: var(--section-pad) var(--gutter);
  background: var(--ink);
  color: var(--on-dark);
}

.band__inner {
  display: grid;
  grid-template-columns: 1.2fr 1fr;
  gap: 64px;
  align-items: center;
  max-width: var(--shell-max);
  margin: 0 auto;
}

.band--unillustrated .band__inner {
  grid-template-columns: 1fr;
}

/* A fixed ratio, so the band's height is the design's and not the source
   photograph's. This is a photograph of a room, not a plate, so it crops. */
.band__image {
  width: 100%;
  aspect-ratio: 4 / 3;
  object-fit: cover;
  object-position: center 70%;
}

/* Without the photograph the one column is the full measure, which is twice
   a readable line, so the text keeps the intro measure either way. */
.band__text {
  display: flex;
  flex-direction: column;
  max-width: 34em;
}

.band__title {
  margin-top: 12px;
  font: 400 var(--size-heading) / var(--leading-heading) var(--font-ui);
  letter-spacing: -0.01em;
  text-wrap: balance;
}

.band__body {
  margin-top: 16px;
  color: var(--on-dark-soft);
  text-wrap: pretty;
}

.band__body + .text-link {
  margin-top: 24px;
}

/* What the scroll says, quoted from the register beneath the link to it.
   Set as a run of names rather than a list, so it reads as cloth, not a menu;
   in the serif, because these are not the site's words. */
.band__register {
  margin-top: 24px;
  font: italic 400 var(--size-lead) / var(--leading-lead) var(--font-display);
  color: var(--on-dark-faint);
  text-wrap: pretty;
}

/* ---------- 12. Workshops ---------- */

.workshops {
  padding: 96px var(--gutter);
}

.workshops__grid {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 64px;
  align-items: center;
  max-width: var(--shell-max);
  margin: 0 auto;
}

.workshops__grid--unillustrated {
  grid-template-columns: 1fr;
}

.workshops__image {
  width: 100%;
  aspect-ratio: 16 / 10;
  object-fit: cover;
}

.workshops__title {
  margin-top: 12px;
  font: 400 var(--size-heading) / var(--leading-heading) var(--font-ui);
  letter-spacing: -0.01em;
  text-wrap: balance;
}

.workshops__body {
  margin-top: 16px;
  max-width: 34em;
  color: var(--text-soft);
  text-wrap: pretty;
}

/* The one live fact in the band, so it keeps the ink the body gives up. */
.workshops__next {
  margin-top: 16px;
  max-width: 34em;
  text-wrap: pretty;
}

.workshops .button-row {
  margin-top: 24px;
}

/* ---------- 13. Exhibitions and news ---------- */

.news-grid {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: 48px;
  margin-top: 32px;
}

/* The rule above each entry is the section boundary doing the work a border
   around a card would otherwise do. */
.news-card {
  display: block;
  border-top: 1px solid var(--ink);
  padding-top: 16px;
}

.news-card__date {
  display: block;
  font: 400 var(--size-caption) / var(--leading-lead) var(--font-ui);
  color: var(--text-soft);
}

.news-card__title {
  display: block;
  margin-top: 8px;
  font: 400 var(--size-lead) / var(--leading-title) var(--font-ui);
}

.news-card:hover .news-card__title {
  text-decoration: underline;
  text-decoration-thickness: 1px;
  text-underline-offset: 4px;
}

/* The way to the newsletter, after the news itself: one line, the group's gap above it. */
.news-invite {
  display: inline-block;
  margin-top: 32px;
}

/* ---------- 14. About and contact ---------- */

/* About and Contact are a pair, so a vertical rule splits them rather than
   two separate sections stacked. */
.pair {
  display: grid;
  grid-template-columns: 1fr 1fr;
  max-width: var(--shell-outer);
  margin: 0 auto;
}

.pair__column {
  padding: var(--section-pad) var(--gutter);
}

.pair__column--ruled {
  border-right: var(--rule);
}

.pair__body {
  margin-top: 16px;
  max-width: 34em;
  color: var(--text-soft);
  text-wrap: pretty;
}

.pair__column .text-link {
  display: inline-block;
  margin-top: 24px;
}

.pair__column .button-row {
  margin-top: 24px;
}

/* ---------- 15. Footer ---------- */

.site-footer {
  display: flex;
  justify-content: space-between;
  gap: 24px;
  max-width: var(--shell-outer);
  margin: 0 auto;
  padding: 32px var(--gutter);
  font: 400 var(--size-caption) / var(--leading-lead) var(--font-ui);
  color: var(--text-soft);
}

.site-footer__links {
  display: flex;
  gap: 24px;
}

.site-footer a:hover {
  color: var(--ink);
}

/* ---------- 16. Paintings page ---------- */

/* The filters read as a contents line rather than a toolbar: two runs of
   plain links split by a rule, the current one underlined the way the
   navigation says "you are here". */
.filters {
  display: flex;
  flex-wrap: wrap;
  gap: 12px 32px;
  margin-top: 40px;
  padding-bottom: 20px;
  border-bottom: var(--rule);
}

.filters__group {
  display: flex;
  flex-wrap: wrap;
  gap: 4px 24px;
  align-items: baseline;
}

.filters__group--availability {
  padding-left: 32px;
  border-left: var(--rule);
}

.filters__chip {
  display: inline-block;
  padding: 4px 0;
  color: var(--text-soft);
}

.filters__chip:hover {
  color: var(--ink);
}

.filters__chip--current {
  color: var(--ink);
  text-decoration: underline;
  text-decoration-thickness: 1px;
  text-underline-offset: 6px;
}

.filters__empty {
  margin-top: 48px;
  color: var(--text-soft);
}

.filters__empty .text-link {
  margin-left: 8px;
}

/* ---------- 17. Painting detail ---------- */

/* The plate and its entry. Two thirds of the measure to the painting, which
   is printed at its own proportion with nothing around it; one third to the
   catalogue entry, which stays in view beside it while the plate scrolls. */
.painting {
  display: grid;
  grid-template-columns: minmax(0, 2fr) minmax(0, 1fr);
  gap: 64px;
  align-items: start;
  max-width: var(--shell-outer);
  margin: 0 auto;
  padding: 48px var(--gutter) 64px;
}

.painting__plate {
  display: flex;
  justify-content: center;
}

/* The width and height attributes give the canvas's proportions, so the box
   is reserved before the file arrives. A landscape canvas fills the column; a
   portrait one is capped to the viewport instead, so the entry beside it is
   never stranded below the fold. */
.painting__image {
  display: block;
  width: 100%;
  height: auto;
}

.painting__image--portrait {
  width: auto;
  max-width: 100%;
  max-height: calc(100vh - var(--header-height) - 96px);
}

.painting__label {
  position: sticky;
  top: calc(var(--header-height) + 48px);
}

.painting__title {
  font: italic 400 var(--size-heading) / var(--leading-heading) var(--font-display);
  text-wrap: balance;
}

/* One fact to a line, as a catalogue prints them. */
.painting__facts {
  margin-top: 16px;
  color: var(--text-soft);
}

.painting__availability {
  color: var(--ink);
}

.painting__price--status {
  color: var(--text-soft);
}

/* The red dot a gallery puts beside a sold work's label. A dot is the one
   round thing on the site: it is a mark, not a box, so the no-radius rule
   does not apply. */
.painting-card__price--sold::before,
.painting__price--sold::before {
  content: "";
  display: inline-block;
  width: 7px;
  height: 7px;
  margin-right: 8px;
  vertical-align: 1px;
  border-radius: 50%;
  background: var(--sold-dot);
}

.painting__description {
  margin-top: 24px;
  max-width: 46ch;
  color: var(--text-soft);
  text-wrap: pretty;
}

.painting__collection {
  display: inline-block;
  margin-top: 24px;
}

/* The works either side, named, on the rule beneath the entry. */
.pager {
  display: flex;
  justify-content: space-between;
  gap: 32px;
  max-width: var(--shell-outer);
  margin: 0 auto;
  padding: 24px var(--gutter) 64px;
  border-top: var(--rule);
}

.pager__link {
  display: inline-flex;
  align-items: center;
  gap: 12px;
  color: var(--text-soft);
}

.pager__link:hover {
  color: var(--ink);
}

.pager__title {
  font: italic 400 var(--size-lead) / var(--leading-title) var(--font-display);
  color: var(--ink);
}

.pager__link--next {
  margin-left: auto;
  text-align: right;
}

/* ---------- 18. Collections page ---------- */

/* A contents page rather than a card wall: each body of work gets a full row,
   the same way round every time, with the rule marking where one ends. The
   cover is a painting, so it is printed whole. */
.collection-row {
  display: grid;
  grid-template-columns: 5fr 7fr;
  gap: 64px;
  align-items: center;
  padding: 48px 0;
  border-top: var(--rule);
}

.collection-row:first-of-type {
  margin-top: 16px;
  border-top: none;
}

/* A tall cover is capped so a row never grows past a screen; a wide one
   fills its column. */
.collection-row__image {
  display: block;
  width: auto;
  max-width: 100%;
  max-height: 560px;
}

.collection-row__name {
  font: 400 var(--size-heading) / var(--leading-heading) var(--font-ui);
  letter-spacing: -0.01em;
  text-wrap: balance;
}

.collection-row__description {
  margin-top: 12px;
  max-width: 34em;
  color: var(--text-soft);
  text-wrap: pretty;
}

.collection-row__count {
  margin-top: 8px;
  font: 400 var(--size-caption) / var(--leading-lead) var(--font-ui);
  font-variant-numeric: tabular-nums;
  color: var(--text-soft);
}

.collection-row__link {
  display: inline-block;
  margin-top: 24px;
}

/* ---------- 19. Save Me!: hero and language ribbon ---------- */

.installation-hero {
  --ribbon-width: 200px;
  --ribbon-overlap: 72px;
  --ribbon-fade: 70px;
  --ribbon-scroll: 46s;

  display: grid;
  grid-template-columns: 1fr var(--ribbon-width) 1.25fr;
  border-bottom: var(--rule);
}

.installation-hero--unribboned {
  --ribbon-overlap: 0px;

  grid-template-columns: 1fr 1.25fr;
}

.installation-hero--unillustrated {
  grid-template-columns: 1fr var(--ribbon-width);
}

.installation-hero--unribboned.installation-hero--unillustrated {
  grid-template-columns: 1fr;
}

.installation-hero__text {
  display: flex;
  flex-direction: column;
  padding: 80px 44px 80px var(--gutter);
}

.installation-hero__title {
  margin-top: 12px;
  font: 400 var(--size-display) / var(--leading-tight) var(--font-ui);
  letter-spacing: -0.015em;
}

.installation-hero__lead {
  margin-top: 16px;
  max-width: 26em;
  font: 400 var(--size-lead) / var(--leading-lead) var(--font-ui);
  text-wrap: pretty;
}

.installation-hero__body {
  margin-top: 16px;
  max-width: 30em;
  color: var(--text-soft);
  text-wrap: pretty;
}

.installation-hero .button-row {
  align-items: flex-start;
}

/* The photograph sits in a frame rather than setting one. Left to itself a
   tall photograph would size the row by its own aspect ratio, so the hero would
   grow with the viewport and strand the title halfway down a wide screen.

   Inside the frame it is contained, never cropped: the scroll is the work, and
   a cover crop cut away most of its fall. The frame takes the mount colour, so
   the column still reads as one block, and the photograph is centred in the
   part the ribbon leaves clear. */
.installation-hero__figure {
  position: relative;
  min-height: 560px;
  background: var(--mount);
}

.installation-hero__image {
  position: absolute;
  top: 0;
  left: var(--ribbon-overlap);
  width: calc(100% - var(--ribbon-overlap));
  height: 100%;
  object-fit: contain;
}

/* The signature element: printed-line cloth, hung in front of the photograph
   the way the scroll itself hangs, with the names rising past the reader. */
.ribbon {
  position: relative;
  z-index: 1;
  overflow: hidden;
  margin-right: calc(-1 * var(--ribbon-overlap));
  border-left: var(--rule);
  background: repeating-linear-gradient(0deg,
      rgb(241 239 233 / 94%) 0 3px,
      rgb(236 233 226 / 94%) 3px 4px);
  box-shadow: 8px 0 24px rgb(var(--ink-rgb) / 0.12);
}

.ribbon__track {
  position: absolute;
  inset: 0 0 auto;
  display: flex;
  flex-direction: column;
}

.ribbon__list {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 24px;
  /* Only the top is padded: the gap alone carries the join, so the halfway
     point of the track lands exactly on the start of the second copy. */
  padding-top: 24px;
  text-align: center;
}

/* Set at 24px rather than the comp's 18px so every step of the fade below
   still clears WCAG's large-text contrast on this cloth. At 18px the same
   range would have to start at .7, which erases the fade altogether. */
/* The names are what the scroll says, not what the site says, so they take
   the serif. */
.ribbon__name {
  font: 400 24px/1.2 var(--font-display);
}

/* The fainter the name, the closer the language is to silence. */
.ribbon__name--vulnerable {
  opacity: 1;
}

.ribbon__name--definitely {
  opacity: 0.85;
}

.ribbon__name--severely {
  opacity: 0.7;
}

.ribbon__name--critically {
  opacity: 0.55;
}

@media (prefers-contrast: more) {
  .ribbon__name {
    opacity: 1;
  }
}

@keyframes ribbon-scroll {
  to {
    transform: translateY(-50%);
  }
}

@media (prefers-reduced-motion: no-preference) {
  .ribbon__track {
    animation: ribbon-scroll var(--ribbon-scroll) linear infinite;
  }

  .ribbon:focus-within .ribbon__track {
    animation-play-state: paused;
  }
}

/* A pointer resting on the names holds them still to be read. Only a real
   pointer: a phone turns a tap into a hover that sticks until the next tap
   elsewhere, so a touch would stop the scroll for good. */
@media (prefers-reduced-motion: no-preference) and (hover: hover) {
  .ribbon:hover .ribbon__track {
    animation-play-state: paused;
  }
}

/* Without motion the ribbon is simply what it is: a column of names to read. */
@media (prefers-reduced-motion: reduce) {
  .ribbon {
    overflow-y: auto;
  }

  .ribbon__track {
    position: static;
  }

  .ribbon__list:last-child,
  .ribbon__fade {
    display: none;
  }
}

.ribbon__fade {
  position: absolute;
  right: 0;
  left: 0;
  height: var(--ribbon-fade);
  pointer-events: none;
}

.ribbon__fade--top {
  top: 0;
  background: linear-gradient(to bottom, var(--paper), transparent);
}

.ribbon__fade--bottom {
  bottom: 0;
  background: linear-gradient(to top, var(--paper), transparent);
}

/* The native player is the base; the behaviour swaps it for this button. */
.soundscape__player {
  width: 100%;
  max-width: 320px;
}

.soundscape__toggle {
  display: flex;
  align-items: center;
  gap: 9px;
  background: none;
  color: inherit;
  cursor: pointer;
  white-space: nowrap;
  line-height: normal;
}

/* An author display beats the user agent's [hidden] rule, so restate it or the
   button would show before Hyperscript has hidden the native controls. */
.soundscape__toggle[hidden] {
  display: none;
}

.soundscape__dot,
.soundscape__bar {
  background: var(--accent);
}

.soundscape__dot {
  width: 8px;
  height: 8px;
}

.soundscape__level {
  display: none;
  align-items: flex-end;
  gap: 2px;
  height: 12px;
}

.soundscape__bar {
  width: 2px;
  height: 12px;
  transform-origin: bottom;
}

.soundscape__toggle.is-playing .soundscape__dot {
  display: none;
}

.soundscape__toggle.is-playing .soundscape__level {
  display: inline-flex;
}

.soundscape__caption {
  flex-basis: 100%;
  font: 400 var(--size-caption) / var(--leading-lead) var(--font-ui);
  color: var(--text-soft);
}

@keyframes soundscape-bar {
  0%,
  100% {
    transform: scaleY(0.3);
  }

  50% {
    transform: scaleY(1);
  }
}

/* The bars only ever move while audio the visitor started is playing, and the
   button beside them is the pause control. */
@media (prefers-reduced-motion: no-preference) {
  .soundscape__toggle.is-playing .soundscape__bar {
    animation: soundscape-bar 0.9s ease-in-out infinite;
  }

  .soundscape__toggle.is-playing .soundscape__bar:nth-child(2) {
    animation-delay: 0.2s;
  }

  .soundscape__toggle.is-playing .soundscape__bar:nth-child(3) {
    animation-delay: 0.4s;
  }
}

/* ---------- 20. Save Me!: the work, views and history ---------- */

.stats {
  display: flex;
  border-bottom: var(--rule);
}

.stats__item {
  flex: 1;
  padding: 36px var(--gutter);
  border-right: var(--rule);
}

.stats__item--note {
  flex: 1.4;
  display: flex;
  align-items: center;
  border-right: none;
}

.stats__figure {
  font: 400 var(--size-display) / var(--leading-tight) var(--font-ui);
  font-variant-numeric: tabular-nums;
  letter-spacing: -0.015em;
}

.stats__figure--accent {
  color: var(--accent);
}

.stats__label {
  margin-top: 8px;
  font: 400 var(--size-caption) / var(--leading-lead) var(--font-ui);
  color: var(--text-soft);
}

.stats__note {
  font: 400 var(--size-lead) / var(--leading-lead) var(--font-ui);
  color: var(--text-soft);
  text-wrap: pretty;
}

.work {
  display: grid;
  grid-template-columns: 1fr 1.15fr;
  gap: 56px;
  align-items: start;
}

.work--unillustrated {
  grid-template-columns: 1fr;
}

.work__title {
  margin-top: 12px;
  font: 400 var(--size-heading) / var(--leading-heading) var(--font-ui);
  letter-spacing: -0.01em;
  text-wrap: balance;
}

.work__body {
  margin-top: 16px;
  max-width: 34em;
  color: var(--text-soft);
  text-wrap: pretty;
}

.work__body:first-of-type {
  margin-top: 20px;
}

/* Her words, not the site's, so the quote takes the serif the titles do. */
.pull-quote {
  margin: 32px 0 0;
  padding: 24px;
  background: var(--mount);
}

.pull-quote__text {
  font: italic 400 var(--size-lead) / var(--leading-lead) var(--font-display);
}

.pull-quote__text::before {
  content: "\201C";
}

.pull-quote__text::after {
  content: "\201D";
}

.pull-quote__attribution {
  display: block;
  margin-top: 12px;
  font: 400 var(--size-caption) / var(--leading-lead) var(--font-ui);
  font-style: normal;
  color: var(--text-soft);
}

.work__figures {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 16px;
  align-self: stretch;
}

.work-figure {
  display: flex;
  flex-direction: column;
  gap: 8px;
}

.work-figure__image {
  width: 100%;
  aspect-ratio: 1;
  object-fit: cover;
}

.work-figure__caption {
  font: 400 var(--size-caption) / var(--leading-lead) var(--font-ui);
  color: var(--text-soft);
}

.gallery {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  grid-auto-rows: 260px;
  gap: 16px;
  margin-top: 28px;
}

/* The first view is the room itself, so it gets the room's share of the grid. */
.gallery__figure {
  position: relative;
}

.gallery__figure:first-child {
  grid-column: span 2;
  grid-row: span 2;
}

/* The link is the cell: the image inside it is absolutely positioned, so
   without a box of its own the anchor would collapse to nothing. */
.gallery__link {
  position: absolute;
  inset: 0;
  display: block;
}

.gallery__image {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

/* An ordered list because the scroll's travels are a sequence, but the numbers are
   not printed: the dates already say the order. The reset takes care of that. */
.timeline {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(210px, 1fr));
  margin-top: 28px;
}

.timeline__entry {
  padding-right: 28px;
}

.timeline__entry + .timeline__entry {
  padding-left: 28px;
  border-left: var(--rule);
}

.timeline__entry:last-child {
  padding-right: 0;
}

.timeline__date {
  font: 400 var(--size-caption) / var(--leading-lead) var(--font-ui);
  color: var(--text-soft);
}

.timeline__title {
  margin-top: 8px;
  font: 500 var(--size-lead) / var(--leading-title) var(--font-ui);
}

.timeline__note {
  margin-top: 8px;
  font: 400 var(--size-caption) / var(--leading-lead) var(--font-ui);
  color: var(--text-soft);
}

.timeline__link {
  display: inline-block;
  margin-top: 12px;
}

/* ---------- 21. Workshops page ---------- */

/* Two across rather than three: these are photographs of people making
   things, and they need the room to read as that. */
.workshop-list {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 64px 48px;
  margin-top: 40px;
}

.workshop__image {
  width: 100%;
  aspect-ratio: 4 / 3;
  object-fit: cover;
}

/* Where and when, before what: the two facts a group co-ordinator scans for. */
.workshop__meta {
  margin-top: 16px;
  font: 400 var(--size-caption) / var(--leading-lead) var(--font-ui);
  color: var(--text-soft);
}

.workshop__title {
  margin-top: 4px;
  font: 500 var(--size-title) / var(--leading-title) var(--font-ui);
  text-wrap: balance;
}

.workshop__body {
  margin-top: 12px;
  max-width: 34em;
  color: var(--text-soft);
  text-wrap: pretty;
}

.closing {
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: 48px;
}

/* Fully booked, running now, or a link to ask: said in words, in the ink. */
.workshop__standing {
  margin-top: 12px;
}

.closing__body {
  margin-top: 12px;
  max-width: 34em;
  color: var(--text-soft);
  text-wrap: pretty;
}

.closing__button {
  flex: none;
}

/* ---------- 22. News page and a single post ---------- */

/* A record read down rather than across: the date holds its own column, so
   the years line up the way they do on a CV. */
.post-list {
  margin-top: 40px;
  border-top: 1px solid var(--ink);
}

.post-entry {
  display: grid;
  grid-template-columns: 160px 1fr;
  gap: 32px;
  padding: 24px 0;
  border-bottom: var(--rule);
}

.post-entry__date {
  font: 400 var(--size-caption) / var(--leading-lead) var(--font-ui);
  font-variant-numeric: tabular-nums;
  color: var(--text-soft);
}

/* An award is not a show: the kind sits under the date in the same quiet size. */
.post-entry__kind {
  margin-top: 2px;
  font: 400 var(--size-caption) / var(--leading-lead) var(--font-ui);
  color: var(--text-soft);
}

.post-entry__title {
  font: 500 var(--size-title) / var(--leading-title) var(--font-ui);
  text-wrap: balance;
}

.post-entry__title a:hover {
  text-decoration: underline;
  text-decoration-thickness: 1px;
  text-underline-offset: 4px;
}

.post-entry__excerpt {
  margin-top: 8px;
  max-width: 34em;
  color: var(--text-soft);
  text-wrap: pretty;
}

.post-paging {
  padding-top: 32px;
}

.post {
  max-width: 44em;
}

.post__meta {
  font: 400 var(--size-caption) / var(--leading-lead) var(--font-ui);
  color: var(--text-soft);
}

.post__title {
  margin-top: 12px;
  font: 400 var(--size-display) / var(--leading-tight) var(--font-ui);
  letter-spacing: -0.015em;
  text-wrap: balance;
}

.post__image {
  display: block;
  width: 100%;
  height: auto;
  margin-top: 32px;
}

.post__body {
  margin-top: 32px;
  max-width: 34em;
  color: var(--text-soft);
  text-wrap: pretty;
}

.post__body p + p {
  margin-top: 16px;
}

.post__link {
  display: inline-block;
  margin-top: 24px;
}

/* ---------- 23. About page ---------- */

/* Her words, then her face, in the order the homepage hero gives text and canvas. */
.bio-intro {
  display: grid;
  grid-template-columns: minmax(0, 34em) 360px;
  justify-content: space-between;
  align-items: start;
  gap: 64px;
}

/* A photograph of someone, not a plate: cropped to its frame. */
.bio-intro__portrait {
  display: block;
  width: 100%;
  height: auto;
  aspect-ratio: 4 / 5;
  object-fit: cover;
  object-position: 50% 30%;
  background: var(--mount);
}

.bio {
  max-width: 34em;
}

.bio__body {
  margin-top: 24px;
  color: var(--text-soft);
  text-wrap: pretty;
}

.bio__body:first-of-type {
  margin-top: 24px;
  font: 400 var(--size-lead) / var(--leading-lead) var(--font-ui);
  color: var(--ink);
}

.cv__section + .cv__section {
  margin-top: 48px;
}

.cv__heading {
  padding-bottom: 12px;
  font: 500 var(--size-lead) / var(--leading-title) var(--font-ui);
  border-bottom: 1px solid var(--ink);
}

/* The year keeps its own column so the dates line up down the page, which is
   how a CV is read. A line without a year simply starts at the text column. */
.cv__entry {
  display: grid;
  grid-template-columns: 160px 1fr;
  gap: 32px;
  padding: 12px 0;
  border-bottom: var(--rule);
}

.cv__year {
  font: 400 var(--size-caption) / var(--leading-body) var(--font-ui);
  font-variant-numeric: tabular-nums;
  color: var(--text-soft);
  padding-top: 2px;
}

.cv__text {
  text-wrap: pretty;
}

/* ---------- 24. Contact and newsletter pages ---------- */

/* The form keeps a form's width; the studio's whereabouts sit beside it. */
.contact {
  display: grid;
  grid-template-columns: minmax(0, 480px) minmax(0, 1fr);
  gap: 64px 96px;
  align-items: start;
}

.contact > .section__title {
  grid-column: 1 / -1;
}

.enquiry {
  margin-top: 24px;
}

/* What a link elsewhere on the site sent the visitor to ask about. Quiet, because
   it is context for the form rather than part of it; a painting's title is set
   as painting titles are everywhere else. */
.enquiry__about {
  margin-bottom: 24px;
  color: var(--text-soft);
}

.enquiry__painting {
  font: italic 400 var(--size-lead) / var(--leading-lead) var(--font-display);
  color: var(--ink);
}

.enquiry__field + .enquiry__field {
  margin-top: 20px;
}

.enquiry__label {
  display: block;
  margin-bottom: 8px;
  font: 400 var(--size-caption) / var(--leading-lead) var(--font-ui);
  color: var(--text-soft);
}

.enquiry input[type="text"],
.enquiry input[type="email"],
.enquiry textarea {
  display: block;
  width: 100%;
  padding: 12px 14px;
  font: 400 var(--size-body) / var(--leading-lead) var(--font-ui);
  color: var(--ink);
  background: var(--paper);
  border: var(--border-secondary);
}

.enquiry textarea {
  resize: vertical;
}

.enquiry input:focus-visible,
.enquiry textarea:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 1px;
}

/* The honeypot is off-screen rather than display:none, which some bots skip. */
.enquiry__trap {
  position: absolute;
  left: -9999px;
  width: 1px;
  height: 1px;
  overflow: hidden;
}

.enquiry__errors,
.enquiry__error {
  font: 400 var(--size-caption) / var(--leading-lead) var(--font-ui);
  color: var(--ink);
}

.enquiry__errors {
  margin-bottom: 24px;
  padding: 12px 16px;
  background: var(--mount);
  border-left: 2px solid var(--accent);
}

.enquiry__error {
  margin-top: 8px;
  color: var(--accent);
}

.enquiry__check,
.enquiry button[type="submit"] {
  margin-top: 24px;
}

.enquiry__check + button[type="submit"] {
  margin-top: 20px;
}

.enquiry-sent {
  grid-column: 1 / -1;
}

.enquiry-sent .text-link {
  display: inline-block;
  margin-top: 24px;
}

.contact__aside {
  padding-top: 24px;
  max-width: 24em;
}

.contact__aside-heading {
  font: 400 var(--size-caption) / var(--leading-lead) var(--font-ui);
  color: var(--text-soft);
}

.contact__aside p + .contact__aside-heading {
  margin-top: 24px;
}

/* One field, so the page keeps a form's width and no second column. */
.newsletter {
  max-width: 480px;
}

.newsletter__link {
  display: inline-block;
  margin-top: 24px;
}

/* What joining means, said once under the button and no louder than a caption. */
.enquiry__note {
  margin-top: 24px;
  max-width: 34em;
  font: 400 var(--size-caption) / var(--leading-lead) var(--font-ui);
  color: var(--text-soft);
}

/* ---------- 25. Not found ---------- */

/* An error page is a place to give directions, so it is sized for reading
   rather than for a large apology. */
.notice {
  max-width: 34em;
  padding: 32px 0 64px;
}

.notice__title {
  font: 400 var(--size-display) / var(--leading-tight) var(--font-ui);
  letter-spacing: -0.015em;
  text-wrap: balance;
}

.notice__body {
  margin-top: 16px;
  color: var(--text-soft);
  text-wrap: pretty;
}

/* ---------- 26. Blow-out ---------- */

/* The work on the wall, with the whole viewport given over to it.

   The ground is the mount colour, not a dark scrim. A dark surround is what
   every lightbox does, and it is wrong here: against black, this painter's
   muted darks read washed out and her lights glare. Galleries hang on
   off-white for that reason, and the site is that colour throughout.

   The square mount is deliberately absent. It exists to give a grid its
   rhythm; there is no grid here, and a square inscribed in a wide viewport
   would leave a landscape canvas smaller than it is on its own page -- the
   opposite of the point. The rule that matters survives: `object-fit: contain`,
   so no canvas is ever cropped. */
.lightbox {
  position: fixed;
  inset: 0;
  width: 100%;
  max-width: 100%;
  height: 100dvh;
  max-height: none;
  margin: 0;
  padding: 0;
  background: var(--mount);
  color: var(--ink);
  border: 0;
  overflow: hidden;
  /* Only matters when the dialog is open but not modal -- that is, when
     Hyperscript never arrived to call showModal(). A modal dialog is in the
     top layer and ignores this; a non-modal one would sit under the sticky
     header, which is z-index 10. */
  z-index: 20;
}

.lightbox::backdrop {
  background: var(--overlay);
}

/* The installation photographs are of a room, not a canvas, and the Save Me!
   page already owns the dark band they belong on. */
.lightbox--dark {
  background: var(--ink);
  color: var(--on-dark);
}

/* The label takes the height it needs and the painting takes what is left, so
   no title, however long, can ever sit on the canvas. */
.lightbox__panel {
  position: absolute;
  inset: 0;
  display: flex;
  flex-direction: column;
  outline: none;
}

.lightbox__image {
  display: block;
  flex: 1 1 0;
  min-height: 0;
  width: 100%;
  padding: 72px 96px 24px;
  box-sizing: border-box;
  object-fit: contain;
}

.lightbox__label {
  position: relative;
  z-index: 1;
  max-width: min(58ch, calc(100% - 200px));
  margin: 0 48px 32px;
  color: inherit;
}

.lightbox__title {
  display: block;
  font: italic 400 var(--size-title) / var(--leading-title) var(--font-display);
}

.lightbox__spec {
  display: block;
  margin-top: 4px;
  font: 400 var(--size-caption) / var(--leading-lead) var(--font-ui);
  color: var(--text-soft);
}

.lightbox--dark .lightbox__spec {
  color: var(--on-dark-soft);
}

.lightbox__link {
  display: inline-block;
  margin-top: 12px;
  font: 500 var(--size-caption) / var(--leading-lead) var(--font-ui);
  color: var(--accent);
  text-decoration: underline;
  text-decoration-thickness: 1px;
  text-underline-offset: 4px;
}

.lightbox__close {
  position: absolute;
  z-index: 1;
  inset: 18px 24px auto auto;
  margin: 0;
}

.lightbox__button {
  min-width: 44px;
  min-height: 44px;
  padding: 8px 14px;
  font: 400 26px/1 var(--font-ui);
  color: inherit;
  background: none;
  border: 0;
  cursor: pointer;
}

/* Full-height strips rather than small targets: easy to hit, quiet until
   wanted, and out of the way of the painting. */
.lightbox__step {
  position: absolute;
  top: 0;
  bottom: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  width: 88px;
  font: 400 30px/1 var(--font-ui);
  color: inherit;
  opacity: 0.55;
}

.lightbox__step--previous {
  left: 0;
}

.lightbox__step--next {
  right: 0;
}

.lightbox__step:hover,
.lightbox__step:focus-visible {
  opacity: 1;
}

.lightbox__button:focus-visible,
.lightbox__step:focus-visible,
.lightbox__link:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}

.lightbox--dark .lightbox__button:focus-visible,
.lightbox--dark .lightbox__step:focus-visible {
  outline-color: var(--on-dark);
}

/* The fade answers the click that opened it, so it is motion with a cause. */
@media (prefers-reduced-motion: no-preference) {
  .lightbox[open] {
    animation: lightbox-in 0.18s ease-out;
  }

  @keyframes lightbox-in {
    from {
      opacity: 0;
    }
  }
}

/* The grid must not scroll behind the blow-out. */
body:has(.lightbox[open]) {
  overflow: hidden;
}

/* A sideways swipe steps through the blow-out, so the browser must not also
   read it as a swipe back through history. */
html:has(.lightbox[open]) {
  overscroll-behavior-x: none;
}

/* ---------- 27. Mobile ---------- */

@media (max-width: 840px) {
  :root {
    --gutter: 20px;
    --header-height: 77px;
    --size-display: 34px;
  }

  .site-header {
    padding: 16px var(--gutter);
  }

  .site-nav__list {
    display: none;
  }

  .menu {
    display: block;
  }

  .hero {
    grid-template-columns: 1fr;
    gap: 40px;
    padding: 48px var(--gutter);
  }

  .section {
    padding: 48px var(--gutter);
  }

  .section--flush {
    padding: 0;
  }

  .workshops {
    padding: 64px var(--gutter);
  }

  .section__head {
    flex-direction: column;
    align-items: flex-start;
    gap: 8px;
  }

  .installation-hero,
  .installation-hero--unribboned,
  .installation-hero--unillustrated {
    --ribbon-overlap: 0px;

    grid-template-columns: 1fr;
  }

  .installation-hero__text {
    padding: 48px var(--gutter);
  }

  /* Stacked, the row no longer has to match the text beside it, so the
     photograph takes its own shape at the full width: whole, and as large as
     the screen allows. */
  .installation-hero__figure {
    min-height: 0;
  }

  .installation-hero__image {
    position: static;
    width: 100%;
    height: auto;
  }

  .ribbon {
    height: 360px;
    border-top: var(--rule);
    border-bottom: var(--rule);
    border-left: none;
    box-shadow: none;
  }

  .stats {
    flex-wrap: wrap;
  }

  .stats__item {
    flex: 1 1 42%;
    padding: 22px var(--gutter);
    border-bottom: var(--rule);
  }

  .stats__item--note {
    flex: 1 1 100%;
  }

  .work {
    grid-template-columns: 1fr;
    gap: 32px;
  }

  .gallery {
    grid-template-columns: repeat(2, minmax(0, 1fr));
    grid-auto-rows: 200px;
  }

  .gallery__figure:first-child {
    grid-column: 1 / -1;
    grid-row: auto;
  }

  .timeline {
    grid-template-columns: 1fr;
  }

  .timeline__entry,
  .timeline__entry + .timeline__entry {
    padding: 22px 0;
    border-top: var(--rule);
    border-left: none;
  }

  .lightbox__image {
    padding: 56px 12px 16px;
  }

  /* Clears the 64px step bar beneath it. */
  .lightbox__label {
    max-width: none;
    margin: 0 20px 80px;
  }

  .lightbox__title {
    font-size: 22px;
  }

  /* Below the caption rather than beside the painting, so a thumb can reach
     both without covering the work. */
  .lightbox__step {
    top: auto;
    bottom: 0;
    width: 50%;
    height: 64px;
    opacity: 1;
  }

  .lightbox__step--previous {
    justify-content: flex-start;
    padding-left: 20px;
  }

  .lightbox__step--next {
    justify-content: flex-end;
    padding-right: 20px;
  }

  .bio-intro {
    grid-template-columns: minmax(0, 1fr);
    gap: 48px;
  }

  .cv__entry {
    grid-template-columns: 1fr;
    gap: 2px;
  }

  .cv__year {
    padding-top: 0;
  }

  .post-entry {
    grid-template-columns: 1fr;
    gap: 8px;
  }

  .post-entry__kind {
    display: inline;
  }

  .workshop-list {
    grid-template-columns: 1fr;
    gap: 48px;
  }

  .closing {
    flex-direction: column;
    align-items: flex-start;
    gap: 24px;
  }

  .collection-row {
    grid-template-columns: 1fr;
    gap: 24px;
    padding: 40px 0;
  }

  .painting {
    grid-template-columns: 1fr;
    gap: 32px;
    padding: 24px var(--gutter) 48px;
  }

  .painting__image--portrait {
    width: 100%;
    max-height: none;
  }

  .painting__label {
    position: static;
  }

  .pager {
    flex-direction: column;
    gap: 16px;
    padding-bottom: 48px;
  }

  .pager__link--next {
    margin-left: 0;
    text-align: left;
  }

  .filters__group--availability {
    padding-left: 0;
    border-left: none;
  }

  /* One plate to a row on a phone; a tablet takes two, below. */
  .painting-grid,
  .collection-grid {
    grid-template-columns: 1fr;
    gap: 48px;
  }

  /* News titles are long; two columns would set them four words wide. */
  .news-grid {
    grid-template-columns: 1fr;
  }

  .workshops__grid,
  .pair {
    grid-template-columns: 1fr;
  }

  /* Flush on a phone: the image spans the screen and the text keeps the
     gutter, so the band gives up its padding and the grid its gap. */
  .band {
    padding: 0;
  }

  .band__inner {
    grid-template-columns: 1fr;
    gap: 0;
  }

  .band__image {
    height: 320px;
  }

  .band__text {
    padding: 48px var(--gutter);
  }

  .pair__column {
    padding: 48px var(--gutter);
  }

  .pair__column--ruled {
    border-right: none;
    border-bottom: 1px solid rgb(var(--ink-rgb) / 0.12);
  }

  .contact {
    grid-template-columns: minmax(0, 1fr);
    gap: 48px;
  }

  .site-footer {
    flex-direction: column;
    gap: 8px;
    padding: 24px var(--gutter) 32px;
  }
}

/* Between a phone and the desktop measure, two plates to a row: one is a
   phone's layout stretched, three are too small to read. */
@media (min-width: 600px) and (max-width: 840px) {
  .painting-grid {
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: 48px 32px;
  }
}

/* ---------- 28. Utility ---------- */

.visually-hidden {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}
