/* Component and layout styles. Tokens live in tokens.css and are the only
   source of colour and spacing values here. */

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

body {
  margin: 0;
  background: var(--color-bg);
  color: var(--color-text);
  font-family: var(--font-body);
  font-size: 15px;
  line-height: 1.55;
  font-weight: 400;
  text-wrap: pretty;
}

h1, h2, h3, h4, h5, h6 {
  font-family: var(--font-heading);
  font-weight: var(--font-heading-weight);
  line-height: 1.12;
  letter-spacing: -0.015em;
  margin: 0 0 var(--space-2);
}
h1 { font-size: 42px; }
h2 { font-size: 32px; }
h3 { font-size: 25px; }
h4 { font-size: 20px; }
h5 { font-size: 16px; }
h6 { font-size: 13px; letter-spacing: 0.08em; text-transform: uppercase; }

p { margin: 0 0 var(--space-3); }
/* height:auto is not optional. Every product image carries width and height
   attributes to reserve layout space; without this the attribute height wins
   while max-width shrinks the width, and the photograph renders stretched. */
img { display: block; max-width: 100%; height: auto; }
figure { margin: 0; }

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

:focus { outline: none; }
:focus-visible { outline: 2px solid var(--color-accent-ink); outline-offset: 2px; }
::selection { background: color-mix(in srgb, var(--color-accent-ink) 30%, transparent); }

.text-muted { color: color-mix(in srgb, var(--color-text) 55%, transparent); }

/* The display face. The negative left margin is an optical correction so the
   cap aligns flush with the rule and the content column beneath it. */
.display,
.page-title {
  font-family: var(--font-heading);
  font-weight: 800;
  line-height: 1.06;
  letter-spacing: -0.02em;
  margin-left: -0.058em;
}

/* The hero headline. */
.display { font-size: clamp(42px, 6.4vw, 84px); }

/* A page heading below a breadcrumb, one step down from the hero. */
.page-title { font-size: clamp(38px, 4.6vw, 62px); }

.label {
  font-size: 12px;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  font-weight: 600;
}

/* — layout — */
.page { min-height: 100vh; display: flex; flex-direction: column; }
/* The page gutter, which also centres content at the 1200px measure. Set
   vertical space on a .wrap with padding-block, never the padding shorthand:
   the shorthand resets padding-inline to zero and the gutter collapses. */
.wrap { padding-inline: var(--page-pad); }
.section { padding-block: 56px; }
.section-lg { padding-block: 104px; }
.rule-top { border-top: var(--rule); }
.rule-bottom { border-bottom: var(--rule); }

/* — buttons — */
.btn {
  display: inline-flex;
  align-items: center;
  justify-content: flex-start; /* labels are flush left, including in buttons */
  gap: 6px;
  cursor: pointer;
  text-decoration: none;
  font-family: var(--font-heading);
  font-weight: var(--font-heading-weight);
  font-size: 14px;
  line-height: 1.2;
  color: var(--color-text);
  background: transparent;
  border: 1px solid transparent;
  padding: var(--space-2) calc(var(--space-3) * 1.2);
  border-radius: var(--radius-md);
}
.btn:disabled { opacity: 0.45; cursor: not-allowed; }
/* The primary states come from named roles rather than ramp steps. The ramp
   inverts per theme and the accent no longer sits mid-ramp, so a step that
   darkened the button on the paper ground lightened it on the dark one. */
.btn-primary { background: var(--color-accent); color: var(--paper); }
.btn-primary:hover { background: var(--color-accent-hover); color: var(--paper); }
.btn-primary:active { background: var(--color-accent-press); }
.btn-secondary { border-color: var(--color-divider); }
/* Solid ink, the design system's other fill. Used where a control has to be
   obviously pressable without spending the accent on it. */
.btn-ink { background: var(--color-text); color: var(--color-bg); }
.btn-ink:hover { background: color-mix(in srgb, var(--color-text) 82%, var(--color-bg)); color: var(--color-bg); }
.btn-secondary:hover { background: color-mix(in srgb, var(--color-text) 7%, transparent); color: var(--color-text); }
.btn-ghost { color: var(--color-accent-ink); padding-inline: var(--space-1); }
.btn-ghost:hover { background: color-mix(in srgb, var(--color-accent-ink) 10%, transparent); }
.btn-icon { width: 36px; height: 36px; padding: 0; justify-content: center; }
/* WhatsApp, in WhatsApp's own green with their mark. The one place another
   company's colour appears: it is a recognised control, and an ink button
   with their glyph reads as a link to something else. Paper type on it, like
   the accent field, and it keeps the system's square corners. */
.btn-whatsapp { background: #25d366; color: #0b1f12; justify-content: center; gap: 10px; }
.btn-whatsapp:hover { background: #1fbb59; color: #0b1f12; }
.btn-whatsapp:active { background: #17a34a; }
.btn-block { width: 100%; }
/* The "Last used" tag inside a sign in button: pushed to the far end so the
   label itself stays flush left, and quieter than the label because it is a
   hint. It inherits the button's own colour, so it is right on the accent
   field, on Google's white and on Google's near black alike. */
.last-used {
  margin-left: auto;
  padding-left: var(--space-2);
  font-family: var(--font-body);
  font-weight: 400;
  font-size: 12px;
  opacity: 0.8;
}
/* Continue with Google. The second company mark on the site after WhatsApp,
   and for the same reason: it is a control people look for. Google's rules put
   their four colour G on white, or on near black in a dark theme, with a
   hairline border, so this follows the theme rather than using ink. The
   colours are theirs and are written here, not taken from tokens. The label
   stays flush left and the corners square, which their rules allow. */
.btn-google {
  background: #ffffff;
  color: #1f1f1f;
  border-color: #747775;
  gap: 12px;
}
.btn-google:hover { background: color-mix(in srgb, #303030 8%, #ffffff); color: #1f1f1f; }
.btn-google:active { background: color-mix(in srgb, #303030 12%, #ffffff); }
html[data-theme="dark"] .btn-google {
  background: #131314;
  color: #e3e3e3;
  border-color: #8e918f;
}
html[data-theme="dark"] .btn-google:hover { background: color-mix(in srgb, #e3e3e3 8%, #131314); color: #e3e3e3; }
html[data-theme="dark"] .btn-google:active { background: color-mix(in srgb, #e3e3e3 12%, #131314); }

/* Choice buttons: sizes, frames, shop tabs. Selected is solid ink. */
.choice {
  font-family: inherit;
  font-size: 13.5px;
  padding: 9px 14px;
  cursor: pointer;
  background: transparent;
  color: var(--color-text);
  border: 2px solid var(--color-divider);
}
.choice:hover { border-color: var(--color-text); }
.choice[aria-pressed="true"], .choice.is-active {
  background: var(--color-text);
  color: var(--color-bg);
  border-color: var(--color-text);
}

/* — forms — */
.input {
  width: 100%;
  min-height: 36px;
  padding: 6px 10px;
  font: inherit;
  font-size: 14px;
  color: var(--color-text);
  caret-color: var(--color-accent-ink);
  background: var(--control-bg);
  border: 1px solid var(--color-divider);
  border-radius: var(--radius-md);
}
.input:hover { border-color: color-mix(in srgb, var(--color-text) 45%, transparent); }
.input:focus-visible { border-color: var(--color-accent-ink); outline-offset: 0; }
.field > label { display: block; font-size: 12px; margin-bottom: 5px; color: color-mix(in srgb, var(--color-text) 70%, transparent); }

/* — tags — */
.tag {
  display: inline-flex;
  align-items: center;
  font-size: 11px;
  letter-spacing: 0.02em;
  padding: 3px 10px;
}
.tag-accent { background: var(--color-accent-100); color: var(--color-accent-800); }
.tag-neutral { background: var(--color-neutral-100); color: var(--color-neutral-800); }
.tag-outline { border: 1px solid var(--color-accent-ink); color: var(--color-accent-ink); }

/* — nav — */
.nav {
  display: flex;
  align-items: center;
  gap: var(--space-4);
  padding: var(--space-3) var(--page-pad);
  border-bottom: var(--rule);
}
.nav-brand {
  display: inline-flex;
  align-items: center;
  gap: 10px;
  font-family: var(--font-heading);
  font-weight: var(--font-heading-weight);
  font-size: 18px;
  margin-right: auto;
}
.nav-links { display: flex; gap: var(--space-4); }
/* Just above the collapse to the menu button, an admin's extra link would push
   the bar a pixel past the viewport. Tighter spacing there, not an earlier
   collapse for everyone. */
@media (max-width: 960px) { .nav-links { gap: var(--space-3); } }
.nav a { font-size: 14px; }
.nav a[aria-current="page"] { color: var(--color-accent-ink); }

/* The nav's controls: search, theme toggle, account, cart. They share the
   control ground so the whole cluster reads as one bar of actions. */
.nav .btn-icon,
.nav .cart-link {
  background: var(--control-bg);
  border: 1px solid var(--color-divider);
  /* Square controls, never squeezed: when the links run long, as they do for
     an admin, the links give way rather than the buttons. */
  flex-shrink: 0;
}
.nav .btn-icon:hover,
.nav .cart-link:hover { border-color: var(--color-text); color: var(--color-accent-ink); }

/* Over the hero photograph the nav is paper type on the scrim, so the controls
   stay transparent rather than punching white boxes through the image. */
.nav-over .nav-search,
.nav-over .btn-icon,
.nav-over .cart-link {
  background: transparent;
  border-color: color-mix(in srgb, var(--paper) 45%, transparent);
}
.nav-over .nav-search input { color: var(--paper); }

/* The theme toggle is a solid ink button with a paper icon, because a thin
   stroked crescent on a white ground is close to invisible. In the dark theme
   the ink token is already near white, so it reverts to the shared control
   ground with the normal text colour. */
.nav .theme-toggle {
  background: var(--color-text);
  color: var(--paper);
  border-color: var(--color-text);
}
.nav .theme-toggle:hover { background: var(--color-accent); border-color: var(--color-accent); }

html[data-theme="dark"] .nav .theme-toggle {
  background: var(--control-bg);
  color: var(--color-text);
  border-color: var(--color-divider);
}
html[data-theme="dark"] .nav .theme-toggle:hover { color: var(--color-accent-ink); }

/* Over the hero photograph it follows the paper treatment like everything else. */
.nav-over .theme-toggle {
  background: transparent;
  color: var(--paper);
  border-color: color-mix(in srgb, var(--paper) 45%, transparent);
}

/* Cart count. Sits on the corner of the cart icon. */
.cart-link { position: relative; display: inline-flex; padding: 8px; }
.cart-badge {
  position: absolute;
  top: -1px;
  right: -1px;
  min-width: 16px;
  height: 16px;
  padding: 0 3px;
  display: flex;
  align-items: center;
  justify-content: center;
  background: var(--color-accent);
  color: var(--paper);
  font-size: 10.5px;
  font-weight: 700;
}

.nav-search {
  display: flex;
  align-items: center;
  gap: 8px;
  min-width: 260px;
  padding: 7px 10px;
  background: var(--control-bg);
  border: 1px solid var(--color-divider);
}
.nav-search:focus-within { outline: 2px solid var(--color-accent-ink); outline-offset: 2px; }
.nav-search input {
  flex: 1;
  border: 0;
  background: transparent;
  font: inherit;
  font-size: 14px;
  color: inherit;
}
.nav-search input::placeholder { color: inherit; opacity: .55; }

/* Hero nav sits over the photograph: paper type, translucent rule. */
.nav-over {
  background: transparent;
  border-bottom: 2px solid color-mix(in srgb, var(--paper) 30%, transparent);
}
.nav-over, .nav-over a { color: var(--paper); }
.nav-over a:hover { color: var(--paper); opacity: .75; }

/* — announcement — */
.announce {
  display: flex;
  justify-content: center;
  padding: 9px 40px;
  font-size: 12.5px;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  border-bottom: var(--rule);
}

/* — masonry grid — */
/* CSS columns, not grid: every photograph keeps its true aspect ratio and the
   column packs around it. The column width is fixed and the count is left to
   the browser, so the grid reflows continuously instead of at breakpoints. */
/* One figure for the space between neighbours, across and down: the column gap
   and the cell's bottom margin both read it, so they cannot drift apart. */
.masonry { --gallery-gap: 12px; columns: 270px; column-gap: var(--gallery-gap); }
.masonry > * { break-inside: avoid; margin-bottom: var(--gallery-gap); }
.masonry > .product-cell { display: block; }

/* On a desktop the gallery stops being a list of cards and becomes a wall of
   photographs: two up, titles off the page, cells nearly touching.

   The count is stated here rather than hinted, which the layout above
   deliberately does not do. Leaving the count to the browser is right while
   the container grows with the viewport. It stops being right above
   --page-max, where the container stops growing at 1120px and a width hint
   can only pin the count: there is no width left between two up and one up to
   ask for. A count also cannot overflow, since columns divide whatever width
   they are given, which is what a fixed track cannot promise. */
@media (min-width: 1000px) {
  .masonry { --gallery-gap: 8px; columns: 2; }

  /* The title is the link's accessible name, so it leaves the page and stays
     in the document. Tab to a cell and it is still announced. Deleting it
     instead would leave the link named only by the alt text, and an
     aria-label would name it for a screen reader while leaving the phone
     layout, which still shows the caption, with nothing. */
  .gallery-caption {
    position: absolute;
    width: 1px;
    height: 1px;
    margin: -1px;
    overflow: hidden;
    clip-path: inset(50%);
    white-space: nowrap;
  }
}

/* Wider than the text column. Prose is capped at --page-max because a measure
   past it is hard to read; a photograph has no such limit, and on a wide
   screen the cap was leaving the work small in the middle of an empty page.
   Only the wall bleeds out, and it bleeds both ways: the heading above it
   stays on the text column, so the gallery's left edge sits outside the
   page's. That is the one place this layout gives up its single left edge,
   and it is deliberate. A one-sided bleed would keep the edge, but its width
   would then be whatever happened to be free on the right, which runs off the
   side of a 1480px screen.

   The bleed is symmetric and derived from the tokens rather than typed, so
   the wall stays centred if either token moves.

   1480px, not 1000px: below it the page padding is only the gutter, so there
   is nothing to bleed into and the negative margin would run off the side.
   1360 plus two 40px gutters is 1440, which clears 1480 with room to spare. */
@media (min-width: 1480px) {
  .masonry {
    --gallery-max: 1360px;
    width: var(--gallery-max);
    margin-inline: calc(
      (var(--page-max) - var(--page-gutter) - var(--page-gutter)
        - var(--gallery-max)) / 2);
  }
}

.gallery-link {
  display: flex;
  flex-direction: column;
  gap: 7px;
  color: inherit;
}
.gallery-link figure { margin: 0; }

/* A cell with several photographs. The frames lie on top of each other and
   the figure takes the shape of the one showing: app.js writes that frame's
   aspect ratio and width onto the figure when it turns, and the transition
   here eases the change, so a portrait after a landscape grows the cell rather
   than being cropped to it. The rest of the column moves with it. The height
   and max-width on the image are the one exception to the rule that an img
   sets height to auto: inside a frame it has to fill the frame, which now has
   its own proportions. */
.cycle {
  position: relative;
  width: 100%;
  transition: aspect-ratio .6s ease, max-width .6s ease;
}
.cycle-frame {
  position: absolute;
  inset: 0;
  opacity: 0;
  transition: opacity .6s ease;
}
.cycle-frame.is-active { opacity: 1; }
.cycle-frame .product-photo,
.cycle-frame .image-slot {
  width: 100%;
  height: 100%;
  max-width: none !important;
  object-fit: cover;
}
@media (prefers-reduced-motion: reduce) {
  .cycle, .cycle-frame { transition: none; }
}

/* The previous and next controls under a photograph on its own page. */
.photo-nav {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  margin-top: 16px;
}
.photo-nav .choice { text-decoration: none; display: inline-flex; align-items: center; }

/* The caption is the title, and it is also the link's accessible name. Small
   and quiet, because in a gallery the photograph is the content and the title
   is a label on it. It is not a hover reveal: most of this traffic has no
   pointer, and a title that appears on hover is a title a phone never sees. */
.gallery-caption {
  font-size: 12px;
  line-height: 18px;
  color: color-mix(in srgb, var(--color-text) 60%, transparent);
}
.gallery-link:hover .gallery-caption { color: var(--color-accent-ink); }

/* The photograph itself. aspect-ratio is set per image from its true pixel
   dimensions, so the slot is correct before the file arrives and the image
   cannot be distorted by a stale width or height attribute. */
/* Photographs are the product. These are deterrents, not protection: they
   stop the easy right click, the drag onto the desktop, and the long press on
   a phone, and they stop nobody who opens the developer tools or takes a
   screenshot. What actually protects the work is that the file behind this is
   a capped copy, served from a private bucket through a link signed for one
   visitor for a few minutes (JOURNAL/0026).

   Scoped to the photographs. The page's text stays selectable: a visitor has
   every right to copy a price, a size, or an address out of it. */
.product-photo, .frame-preview img, .digital-preview img {
  -webkit-user-select: none;
  user-select: none;
  -webkit-user-drag: none;
  -webkit-touch-callout: none;
}
.product-photo { width: 100%; object-fit: cover; }
.card-title { font-family: var(--font-heading); font-weight: 800; font-size: 16px; line-height: 1.2; }

/* A slot awaiting the real photograph. Holds the exact space the image will
   take, so nothing shifts when it lands. */
.image-slot {
  width: 100%;
  background: var(--color-surface);
  border: 1px dashed var(--color-divider);
  display: grid;
  place-items: center;
  padding: var(--space-4);
}
.image-slot span {
  font-size: 11px;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: color-mix(in srgb, var(--color-text) 50%, transparent);
  text-align: center;
}

/* — red poster banner: the only place red is a field — */
.banner {
  background: var(--color-accent);
  color: var(--paper);
  padding-block: 72px;
}
.banner h2, .banner p, .banner a { color: var(--paper); }
.banner .input {
  background: transparent;
  border: 1px solid var(--paper);
  color: var(--paper);
}
.banner .input::placeholder { color: var(--paper); opacity: .85; }
/* The signup form answers where it stands, and it stands on the red field, so
   its label, button, and error are paper rather than the ordinary ink and red
   on red. */
.banner .field > label { color: var(--paper); }
.banner .btn-secondary { border-color: var(--paper); color: var(--paper); }
.banner .form-error {
  border-color: var(--paper);
  background: transparent;
  color: var(--paper);
}

/* The donate banner sits straight after the newsletter's accent field, so it is
   the ink field instead: two blue slabs in a row would spend the accent twice
   in one screen. Ink inverts per theme, like .btn-ink, so its type is the page
   ground. The button on it is still the accent, because it asks for something. */
.banner-ink {
  background: var(--color-text);
  color: var(--color-bg);
  padding-block: 72px;
}
.banner-ink h2, .banner-ink p { color: var(--color-bg); }
.banner-ink .banner-copy p { margin: 0 0 12px; max-width: 52ch; }

/* — numbered feature rows — */
.rows { border-top: var(--rule); }
.row-item {
  display: grid;
  grid-template-columns: 120px 1fr;
  gap: var(--space-6);
  padding-block: 32px;
  border-bottom: var(--rule);
}
.row-num { font-family: var(--font-heading); font-weight: 800; font-size: 20px; color: var(--color-accent-ink); }
@media (max-width: 700px) { .row-item { grid-template-columns: 1fr; gap: var(--space-2); } }

/* — footer — */
.footer { margin-top: auto; border-top: var(--rule); padding-block: 56px; }
.footer-cols { display: grid; grid-template-columns: 2fr 1fr 1fr 1.2fr; gap: var(--space-8); }
.footer-col { display: flex; flex-direction: column; gap: 9px; }
.footer-col a { font-size: 14px; }
@media (max-width: 800px) { .footer-cols { grid-template-columns: 1fr 1fr; } }

/* — tables — */
.table { width: 100%; border-collapse: collapse; font-size: 14px; }
.table th {
  text-align: left;
  font-size: 11px;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: color-mix(in srgb, var(--color-text) 60%, transparent);
  padding: var(--space-2);
  border-bottom: var(--rule);
}
.table td { padding: var(--space-2); border-bottom: 1px solid var(--color-divider); }

/* — pagination — */
/* Wraps because it has to: prev, five numbers and next do not fit across a
   390px screen, and a row that cannot wrap is what scrolls the page sideways. */
.pagination { display: flex; flex-wrap: wrap; justify-content: center; gap: 8px; margin-top: 64px; }

/* — endless scroll — */
/* The shop's marker reaches most of a screen up behind the grid, so the next
   page is asked for before the reader arrives at the bottom rather than once
   they are staring at it. It must not catch a click meant for a photograph it
   overlaps. The words show only while a page is on its way. */
.shop-more {
  margin-top: -80vh;
  padding-top: 80vh;
  pointer-events: none;
}
.shop-more-status { display: block; font-size: 13px; margin-top: 24px; visibility: hidden; }
.shop-more.htmx-request .shop-more-status { visibility: visible; }
/* app.js sets has-htmx. Without it the marker is inert and the pager is the
   way on, so it stays. */
.has-htmx .shop-pager { display: none; }

/* — htmx — */
/* Fade the results while a search request is in flight. */
.htmx-request .htmx-fade { opacity: .5; transition: opacity 120ms ease; }

/* — product page — */
.product-layout-inner { display: grid; grid-template-columns: 7fr 5fr; gap: 48px; }
@media (max-width: 900px) { .product-layout-inner { grid-template-columns: 1fr; } }

/* The story under the photograph, in the photograph's own column, separated
   by the section rule rather than by a box. */
.product-story { margin-bottom: 32px; border-bottom: var(--rule); padding-bottom: 20px; max-width: 62ch; }

.option-row { display: flex; gap: 8px; flex-wrap: wrap; }
/* The choices are links, so they need the button's centred label rather than
   the flush-left one used everywhere else. */
.option-row .choice { text-decoration: none; display: inline-flex; align-items: center; }

/* The frame preview: a dummy frame showing how the piece is mounted.

   Moulding and mat widths arrive as percentages of the real piece, computed
   from the selected size in millimetres, so the same frame reads heavy on a
   small print and slim on a large one. The mat is paper white in both themes,
   because a mat is a physical card and does not change colour with the UI. */
.frame-preview { max-width: 560px; }
.frame-moulding {
  padding: var(--moulding, 4%);
  background: var(--moulding-color, var(--color-text));
  /* A frame is a physical object on a wall, so it casts a shadow. This is the
     one place elevation is used on the storefront.

     The inset hairline is the outer edge of the moulding. Without it a white
     frame on the paper ground has no visible boundary at all. */
  box-shadow:
    var(--shadow-lg),
    inset 0 0 0 1px color-mix(in srgb, var(--color-text) 25%, transparent);
}
.frame-mat {
  padding: var(--mat, 0%);
  background: var(--paper);
  /* The rebate: where the moulding meets the mat. */
  box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--color-text) 18%, transparent);
}
/* The bevel cut around the mat opening, which is what makes a mounted piece
   read as mounted rather than as an image on a coloured block. */
.frame-mat img { box-shadow: 0 0 0 1px color-mix(in srgb, var(--color-text) 28%, transparent); }
/* No special case is needed for "No mat": --mat resolves to 0%, the padding
   collapses, and the paper ground has no width to show through. */
.frame-mat img { width: 100%; }

.digital-preview { max-width: 560px; }

/* A choice with a secondary line, such as a paper size and its dimensions. */
.choice-stack { display: flex; flex-direction: column; gap: 2px; align-items: flex-start; }
.choice-sub { font-size: 11.5px; opacity: .7; letter-spacing: .01em; }

/* — long-form document (terms, policies) — */
/* A reading column, not the numbered feature grid used on marketing pages.
   Measure is capped in ch so it tracks the type size rather than the viewport,
   and it stays flush left like everything else in the system.

   Never put max-width on .wrap itself: .wrap supplies the page gutter through
   padding-inline, and capping its width leaves the padding at full size, which
   crushes the content into a narrow strip. Cap an element inside it. */
/* A document sits beside its own contents index, so a long policy fills the
   measure rather than stranding the text in a column on the left. */
.doc-layout { display: grid; grid-template-columns: 220px minmax(0, 68ch); gap: 64px; align-items: start; }
@media (max-width: 900px) { .doc-layout { grid-template-columns: 1fr; gap: 32px; } }

.doc-index { position: sticky; top: 24px; }
.doc-index ol { list-style: none; margin: 0; padding: 0; counter-reset: doc; }
.doc-index li { counter-increment: doc; }
.doc-index a {
  display: flex;
  gap: 10px;
  padding: 7px 0;
  font-size: 13.5px;
  line-height: 1.3;
  border-bottom: 1px solid var(--color-divider);
}
.doc-index a::before {
  content: counter(doc, decimal-leading-zero);
  font-family: var(--font-heading);
  font-weight: 800;
  font-size: 11px;
  letter-spacing: 0.06em;
  color: var(--color-accent-ink);
  padding-top: 2px;
}
@media (max-width: 900px) { .doc-index { position: static; } }

.doc { max-width: 68ch; }
.doc-section { padding-block: 28px; border-top: var(--rule); }
.doc-section h2 { font-size: 22px; margin: 0 0 10px; }
.doc-section p { font-size: 16px; line-height: 27px; margin: 0 0 12px; }
.doc-section p:last-child { margin-bottom: 0; }
.doc-num {
  font-family: var(--font-heading);
  font-weight: 800;
  font-size: 13px;
  letter-spacing: 0.08em;
  color: var(--color-accent-ink);
  margin: 0 0 6px;
}


/* — cart — */
.cart-layout { display: grid; grid-template-columns: minmax(0, 1fr) 340px; gap: 48px; align-items: start; }
@media (max-width: 900px) { .cart-layout { grid-template-columns: 1fr; gap: 32px; } }

.cart-line {
  display: grid;
  grid-template-columns: 96px minmax(0, 1fr) auto auto;
  gap: 20px;
  align-items: start;
  padding-block: 20px;
  border-bottom: 1px solid var(--color-divider);
}
.cart-line:first-child { border-top: var(--rule); }
@media (max-width: 700px) {
  .cart-line { grid-template-columns: 72px minmax(0, 1fr); }
  .cart-line .cart-total { grid-column: 2; text-align: left; }
}

.cart-thumb { display: block; width: 96px; }
.cart-thumb img { width: 96px; height: 96px; object-fit: cover; }
.cart-thumb-empty { display: block; width: 96px; height: 96px; background: var(--color-surface); border: 1px dashed var(--color-divider); }

/* The stepper is a 2px box, matching the rules that divide the page. */
.qty { display: inline-flex; align-items: center; border: 2px solid var(--color-divider); }
.qty-btn {
  width: 32px; height: 32px;
  display: grid; place-items: center;
  background: transparent; border: 0; cursor: pointer;
  font: inherit; font-size: 15px; color: var(--color-text);
}
.qty-btn:hover { background: color-mix(in srgb, var(--color-text) 8%, transparent); color: var(--color-accent-ink); }
.qty-value { min-width: 32px; text-align: center; font-size: 14px; }

.cart-total { font-family: var(--font-heading); font-weight: 800; font-size: 15px; margin: 0; text-align: right; }

.cart-summary { border: var(--rule); padding: 24px; }
.summary-row { display: flex; justify-content: space-between; gap: 16px; font-size: 14px; padding-block: 8px; }
.summary-total {
  border-top: var(--rule);
  margin-top: 8px;
  padding-top: 16px;
  font-family: var(--font-heading);
  font-weight: 800;
  font-size: 17px;
}

/* — checkout — */
.checkout-layout { display: grid; grid-template-columns: minmax(0, 1fr) 360px; gap: 56px; align-items: start; }
@media (max-width: 900px) { .checkout-layout { grid-template-columns: 1fr; gap: 32px; } }

.checkout-step { padding-block: 28px; border-top: var(--rule); }
.checkout-step:first-of-type { border-top: 0; padding-top: 0; }
.checkout-step h2 { font-size: 22px; margin: 0 0 16px; }

.field-row { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 12px; }
@media (max-width: 700px) { .field-row { grid-template-columns: 1fr; } }

/* A selectable card: delivery method, payment method. */
.pick {
  display: flex;
  align-items: center;
  gap: 16px;
  padding: 14px 16px;
  border: 2px solid var(--color-divider);
  cursor: pointer;
}
.pick + .pick { margin-top: 8px; }
.pick:hover { border-color: var(--color-text); }
.pick.is-active { border-color: var(--color-accent-ink); }
.pick input { position: absolute; opacity: 0; width: 0; height: 0; }
.pick:has(input:focus-visible) { outline: 2px solid var(--color-accent-ink); outline-offset: 2px; }
.pick-body { display: flex; flex-direction: column; gap: 2px; flex: 1; min-width: 0; }
.pick-title { font-family: var(--font-heading); font-weight: 800; font-size: 15px; }
.pick-price { font-family: var(--font-heading); font-weight: 800; font-size: 15px; white-space: nowrap; }

.checkout-summary { border: var(--rule); padding: 24px; position: sticky; top: 24px; }
@media (max-width: 900px) { .checkout-summary { position: static; } }

.summary-item { display: flex; justify-content: space-between; gap: 16px; padding-block: 10px; font-size: 14px; }
.summary-item + .summary-item { border-top: 1px solid var(--color-divider); }
.summary-item-title { font-family: var(--font-heading); font-weight: 800; font-size: 14px; }

/* A standing statement of fact, such as payment not being connected. */
.notice { border: var(--rule); padding: 16px 20px; }
.notice p { font-size: 14px; line-height: 23px; }

.form-error {
  border: 2px solid var(--color-accent-ink);
  color: var(--color-accent-700);
  background: var(--color-accent-100);
  padding: 12px 16px;
  margin: 0 0 24px;
  font-size: 14px;
}

/* — account and dashboard — */
.account-layout { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 32px; align-items: start; }
@media (max-width: 800px) { .account-layout { grid-template-columns: 1fr; } }

.account-card { border: var(--rule); padding: 24px; }

.dash-layout { display: grid; grid-template-columns: minmax(0, 1fr) 320px; gap: 48px; align-items: start; }
@media (max-width: 900px) { .dash-layout { grid-template-columns: 1fr; gap: 32px; } }

/* A checkbox with its label, sized so the box does not shrink beside long text. */
.check { display: flex; align-items: flex-start; gap: 10px; font-size: 14px; cursor: pointer; }
.check input { width: 16px; height: 16px; flex: none; margin-top: 3px; accent-color: var(--color-accent); }

.download-row {
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: 16px;
  padding-block: 14px;
  border-bottom: 1px solid var(--color-divider);
}
.download-row:first-of-type { border-top: var(--rule); }

/* — about — */
.about-hero { display: grid; grid-template-columns: minmax(0, 1fr) 360px; gap: 56px; align-items: center; }
@media (max-width: 900px) { .about-hero { grid-template-columns: 1fr; gap: 32px; } }
.about-portrait { margin: 0; }
.about-portrait img { width: 100%; height: auto; }

/* — search band — */
/* Its own row under the header. Centred, and wide enough that a photograph's
   name is comfortable to type and read back. */
.searchband {
  display: flex;
  justify-content: center;
  padding: 24px var(--page-pad);
  border-bottom: var(--rule);
}
.searchband-form {
  display: flex;
  align-items: center;
  gap: 12px;
  width: min(680px, 100%);
  padding: 6px 6px 6px 16px;
  background: var(--control-bg);
  border: 2px solid var(--color-divider);
}
.searchband-form:focus-within { border-color: var(--color-accent-ink); }
.searchband-form > svg { flex: none; opacity: .6; }
.searchband-form input {
  flex: 1;
  min-width: 0;
  border: 0;
  background: transparent;
  font: inherit;
  font-size: 16px;
  padding-block: 10px;
  color: inherit;
}
.searchband-form input:focus-visible { outline: none; }
.searchband-form input::placeholder { color: inherit; opacity: .5; }
.searchband-form .btn { flex: none; padding-inline: 20px; align-self: stretch; }
@media (max-width: 600px) {
  .searchband-form .btn { display: none; }
}

/* — the walls' search: above the wall on Preview and every room page, and
   larger than the site's search band, because here it is the main way in. */
.walls-search { border: var(--rule); padding: 20px 24px; }
.walls-search-form .label { display: block; margin-bottom: 10px; }
.walls-search-field {
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 6px 6px 6px 16px;
  background: var(--control-bg);
  border: 2px solid var(--color-text);
}
.walls-search-field:focus-within { border-color: var(--color-accent-ink); }
.walls-search-field > svg { flex: none; opacity: .7; }
.walls-search-field input {
  flex: 1;
  min-width: 0;
  border: 0;
  background: transparent;
  font: inherit;
  font-size: 22px;
  padding-block: 12px;
  color: inherit;
}
.walls-search-field input:focus-visible { outline: none; }
.walls-search-field input::placeholder { color: inherit; opacity: .5; }
.walls-search-field .btn { flex: none; padding-inline: 24px; align-self: stretch; }
.walls-search-ideas { display: flex; flex-wrap: wrap; gap: 6px 16px; margin: 12px 0 0; font-size: 14px; }
.walls-search-ideas a { color: inherit; }
.walls-found { margin-top: 32px; }
.walls-found-none { margin: 24px 0 0; }
@media (max-width: 600px) {
  .walls-search { padding: 16px; }
  .walls-search-field input { font-size: 18px; }
  .walls-search-field .btn { padding-inline: 14px; }
}

/* — admin — */
.admin-card { border: var(--rule); padding: 24px; margin-bottom: 24px; }
/* Cart lines whose photograph left the shop: present, legible, and plainly
   not part of the order. */
.cart-gone { margin-top: 32px; border-top: var(--rule); padding-top: 24px; }
.cart-line.is-gone { opacity: 0.72; }

/* The catalogue tab: filters beside the list, one column on a narrow screen
   where a sidebar would only push the work below the fold. */
.admin-catalogue { display: grid; grid-template-columns: 260px minmax(0, 1fr); gap: 32px; align-items: start; }
.admin-filters { border: var(--rule); padding: 20px; position: sticky; top: 20px; }
.admin-add { margin-top: 20px; border-top: 1px solid var(--color-divider); padding-top: 14px; }
.admin-add summary {
  cursor: pointer;
  font-family: var(--font-heading);
  font-weight: var(--font-heading-weight);
  font-size: 15px;
}
@media (max-width: 900px) {
  .admin-catalogue { grid-template-columns: 1fr; gap: 24px; }
  .admin-filters { position: static; }
}

/* The catalogue as pictures. Each photograph is a closed <details>; opening
   one widens it to the whole row so the form inside is not squeezed into a
   column, and closing it puts the grid back. */
.admin-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(210px, 1fr));
  gap: 20px;
  align-items: start;
}
.admin-item { border: var(--rule); padding: 12px; }
.admin-item[open] { grid-column: 1 / -1; padding: 24px; }
.admin-item > summary { cursor: pointer; list-style: none; display: grid; gap: 6px; }
.admin-item > summary::-webkit-details-marker { display: none; }
.admin-item-thumb img, .admin-item-thumb .image-slot { width: 100%; }
.admin-item[open] .admin-item-thumb { display: none; }
.admin-item-title {
  font-family: var(--font-heading);
  font-weight: var(--font-heading-weight);
  font-size: 15px;
}
.admin-item-meta { font-size: 12.5px; }
.admin-item-body { margin-top: 20px; border-top: 1px solid var(--color-divider); padding-top: 20px; }

.admin-delete { margin-top: 20px; border-top: 1px solid var(--color-divider); padding-top: 14px; }
.admin-delete summary { cursor: pointer; font-size: 13.5px; color: var(--color-text); }
/* A deleted photograph is a row, not an editor: a thumbnail and what happened
   to it. One column on a phone. */
.admin-deleted { display: grid; grid-template-columns: 120px 1fr; gap: 20px; align-items: start; }
.admin-deleted-thumb img { width: 100%; }
@media (max-width: 520px) { .admin-deleted { grid-template-columns: 1fr; } .admin-deleted-thumb { max-width: 160px; } }

/* One editable record per row. Wraps rather than scrolling, so nothing is
   hidden off the edge on a laptop. */
.admin-row {
  display: flex;
  flex-wrap: wrap;
  gap: 12px;
  align-items: end;
  padding: 16px;
  border: 2px solid var(--color-divider);
  margin-bottom: 12px;
}
.admin-row .field { min-width: 110px; }
.admin-row .check { align-self: center; }

/* Sizes drawn to scale relative to each other, so the ladder is legible. */
.size-preview {
  border: 2px solid var(--color-text);
  align-self: end;
  flex: none;
}

.admin-images { display: grid; grid-template-columns: repeat(auto-fill, minmax(180px, 1fr)); gap: 20px; }
.admin-image img { width: 100%; }

/* About's notes: the 2px rules of the design system without the numbered
   column, which implied a sequence that does not exist. */
/* Rules run the full measure, as they do everywhere else in the system; only
   the text is constrained to a readable column. */
.about-notes { border-top: var(--rule); }
.about-note { padding-block: 32px; border-bottom: var(--rule); }
.about-note h2 { font-size: 22px; margin: 0 0 10px; }
.about-note p { margin: 0; max-width: 62ch; font-size: 16px; line-height: 27px; }

/* — error pages — */
.error-page { position: relative; padding-block: 32px 72px; }
/* The crop marks sit behind the type, large and faint: the brand's own framing
   device, framing nothing. */
.error-mark {
  position: absolute;
  top: -8px;
  right: 0;
  width: min(420px, 40vw);
  color: var(--color-divider);
  pointer-events: none;
  z-index: -1;
}
@media (max-width: 700px) { .error-mark { display: none; } }

/* — mobile menu — */
/* A checkbox, not a <details>. A closed <details> hides its contents through
   content-visibility in the browser's own stylesheet, which a display rule on
   the child does not override, so the desktop links disappeared entirely. The
   links now live outside the toggle and are shown by default; only the narrow
   breakpoint hides them behind the checkbox. */

/* The nav is the positioning context for the panel that drops out of it.
   Without this the panel offsets from the viewport instead, and top:100% puts
   it a whole screen below the fold, where it looks simply absent. */
.nav { position: relative; }

.nav-menu { display: none; }

/* Visually hidden but still focusable, so the toggle is keyboard operable with
   no JavaScript: tab to it, press space. */
.nav-check,
.visually-hidden {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  clip: rect(0 0 0 0);
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

@media (max-width: 860px) {
  .nav { gap: var(--space-2); }

  /* Menu first, then the brand, then the icon cluster. */
  .nav-menu { display: block; order: -1; }
  .nav-brand { margin-right: auto; }

  .nav-toggle {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 38px;
    height: 38px;
    cursor: pointer;
    background: var(--control-bg);
    border: 1px solid var(--color-divider);
    color: var(--color-text);
  }
  .nav-toggle:hover { border-color: var(--color-text); color: var(--color-accent-ink); }
  .nav-menu:has(.nav-check:focus-visible) .nav-toggle {
    outline: 2px solid var(--color-accent-ink);
    outline-offset: 2px;
  }

  /* The icon states the toggle: bars when shut, a cross when open. */
  .nav-toggle-close { display: none; }
  .nav-menu:has(.nav-check:checked) .nav-toggle-open { display: none; }
  .nav-menu:has(.nav-check:checked) .nav-toggle-close { display: inline-flex; }

  /* The panel drops below the whole header, full width, with tap-sized links. */
  .nav-links {
    display: none;
    position: absolute;
    top: 100%;
    left: 0;
    right: 0;
    z-index: 20;
    flex-direction: column;
    gap: 0;
    padding: var(--space-2) var(--page-pad) var(--space-4);
    background: var(--color-bg);
    border-bottom: var(--rule);
  }
  .nav:has(.nav-check:checked) .nav-links { display: flex; }

  .nav-links a {
    padding: 14px 0;
    font-size: 16px;
    border-bottom: 1px solid var(--color-divider);
  }
  .nav-links a:last-child { border-bottom: 0; }

  /* Over the hero photograph the panel needs its own ground. */
  .nav-over .nav-links { background: #191817; }
  .nav-over .nav-links a { color: var(--paper); }
}

/* — responsive grids —
   These were inline three- and two-column grids with no fallback, which is why
   the home, about, and contact pages scrolled sideways on a phone. */
/* How it works: three steps, each under its own 2px rule, which is the
   section rule the handoff already uses rather than a new device. The numbers
   are ink, not the accent: they label the steps, they do not ask for
   anything. */
.steps {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: 28px;
  margin: 0;
  padding: 0;
  list-style: none;
  counter-reset: step;
}
.step { border-top: var(--rule); padding-top: 16px; }
.step-mark {
  display: flex;
  align-items: center;
  gap: 10px;
  color: color-mix(in srgb, var(--color-text) 55%, transparent);
  margin-bottom: 8px;
}
.step-number {
  display: block;
  font-family: var(--font-heading);
  font-weight: var(--font-heading-weight);
  font-size: 13px;
  letter-spacing: 0.08em;
}
.step-title { font-size: 20px; margin: 0 0 8px; }
.step p { margin: 0 0 8px; }
.step p:last-child { margin-bottom: 0; }
@media (max-width: 800px) { .steps { grid-template-columns: 1fr; gap: 20px; } }

.banner-grid { display: grid; grid-template-columns: 1.1fr 1fr; gap: 48px; align-items: center; }
@media (max-width: 800px) { .banner-grid { grid-template-columns: 1fr; gap: 24px; } }

.contact-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 48px; align-items: start; }
@media (max-width: 800px) { .contact-grid { grid-template-columns: 1fr; gap: 32px; } }

/* On a phone the menu leads, then the brand, then the icon cluster. The toggle
   is what a thumb reaches for first, so it goes first. */
@media (max-width: 860px) {
  .nav-menu { order: -1; margin-right: 0; }
  .nav-brand { margin-right: auto; }
}

/* — sticky header — */
/* The header follows the page down. The search band goes with it, since a
   storefront's search should be reachable from anywhere in a long grid. */
.page-header {
  position: sticky;
  top: 0;
  z-index: 30;
  background: var(--color-bg);
}
/* Over the hero photograph the header is part of the picture, so it scrolls
   away with it rather than detaching onto the page below. */
.page-header.is-over-image { position: static; background: transparent; }

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

/* — back to top — */
.to-top {
  position: fixed;
  right: 24px;
  bottom: 24px;
  z-index: 40;
  display: grid;
  place-items: center;
  width: 44px;
  height: 44px;
  color: var(--paper);
  background: var(--color-text);
  border: 2px solid var(--color-text);
  box-shadow: var(--shadow-md);
}
.to-top:hover { background: var(--color-accent); border-color: var(--color-accent); color: var(--paper); }
.to-top:focus-visible { outline: 2px solid var(--color-accent-ink); outline-offset: 3px; }

/* Hidden until the reader is far enough down for it to be worth offering.
   Driven by a scroll-linked animation, so it needs no JavaScript. */
@supports (animation-timeline: scroll()) {
  .to-top {
    opacity: 0;
    visibility: hidden;
    animation: to-top-in linear both;
    animation-timeline: scroll();
    animation-range: 600px 800px;
  }
  @keyframes to-top-in {
    from { opacity: 0; visibility: hidden; }
    to   { opacity: 1; visibility: visible; }
  }
}
@media (max-width: 600px) { .to-top { right: 16px; bottom: 16px; } }

/* — black and white finish — */
/* The catalogue is never shown desaturated; this applies only to a preview the
   customer has explicitly asked to see converted. The contrast lift matches the
   design system's own grayscale treatment, since a straight desaturation reads
   flat. */
.frame-preview.is-mono .frame-mat img,
.digital-preview.is-mono img {
  filter: grayscale(1) contrast(1.08);
}

/* — wishlist — */
.product-cell { position: relative; break-inside: avoid; }
.product-cell .gallery-link { margin-bottom: 0; }

/* The save toggle sits on the photograph, outside the link, so pressing it
   never navigates. */
.wish-form { position: absolute; top: 10px; right: 10px; z-index: 2; }
.wish-btn {
  display: grid;
  place-items: center;
  width: 36px;
  height: 36px;
  cursor: pointer;
  color: var(--color-text);
  background: var(--control-bg);
  border: 1px solid var(--color-divider);
}
.wish-btn:hover { color: var(--color-accent-ink); border-color: var(--color-accent-ink); }
.wish-btn.is-saved { color: var(--color-accent-ink); border-color: var(--color-accent-ink); }
.wish-btn:focus-visible { outline: 2px solid var(--color-accent-ink); outline-offset: 2px; }

/* On the product page it sits under the buy button, as a labelled control. */
.product-layout-inner .wish-form { position: static; margin-top: 12px; }
.product-layout-inner .wish-btn { width: auto; height: auto; gap: 8px; grid-auto-flow: column; padding: 9px 14px; }
.product-layout-inner .wish-btn .visually-hidden {
  position: static;
  width: auto;
  height: auto;
  margin: 0;
  overflow: visible;
  clip: auto;
  clip-path: none;
  font-size: 14px;
  font-family: var(--font-heading);
  font-weight: 800;
}

/* The border on a delivered file: the same proportion as the card border on a
   mounted print, drawn as paper around the photograph.

   The hairline is not decoration. The border is paper white and the page ground
   is paper white, so without an edge the file's extent is invisible and a
   bordered file looks identical to an unbordered one. */
.digital-mat {
  padding: var(--mat, 0%);
  background: var(--paper);
  box-shadow: 0 0 0 1px color-mix(in srgb, var(--color-text) 22%, transparent);
}

/* — collections — */
.collection-list { border-top: var(--rule); }
.collection-row {
  display: block;
  padding-block: 28px;
  border-bottom: var(--rule);
  color: inherit;
}
.collection-row h2 { font-size: 25px; margin: 0 0 8px; }
.collection-row p { max-width: 62ch; margin: 0 0 10px; }
.collection-row:hover h2 { color: var(--color-accent-ink); }

/* — tags — */
.tag-row { display: flex; flex-wrap: wrap; gap: 8px; }
.tag-link {
  display: inline-flex;
  align-items: center;
  padding: 5px 12px;
  font-size: 12.5px;
  color: var(--color-text);
  border: 1px solid var(--color-divider);
}
.tag-link:hover { border-color: var(--color-accent-ink); color: var(--color-accent-ink); }

/* ── Invoice ──────────────────────────────────────────────────────────────
   A document rather than a page. It is the one screen designed to be
   printed, so the rules below are about what survives paper. */
.invoice-sheet { border: var(--rule); padding: 40px; max-width: 780px; }
.invoice-actions { display: flex; gap: 12px; flex-wrap: wrap; margin-bottom: 28px; }

.invoice-head {
  display: flex; justify-content: space-between; align-items: flex-start;
  gap: 24px; flex-wrap: wrap;
  border-bottom: var(--rule); padding-bottom: 20px; margin-bottom: 28px;
}
.invoice-status {
  margin: 0; font-family: var(--font-heading); font-weight: 800;
  font-size: 13px; letter-spacing: 0.08em; text-transform: uppercase;
  border: var(--rule); padding: 8px 14px; white-space: nowrap;
}
/* Paid is stated, never celebrated: no provider is connected and the merchant
   sets this by hand. Due is the accent, because it is the one that asks for
   something. */
.invoice-status[data-paid="false"] { color: var(--paper); background: var(--color-accent); border-color: var(--color-accent); }

.invoice-parties { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 32px; margin-bottom: 28px; }
.invoice-line { margin: 0 0 2px; font-size: 14px; line-height: 21px; }

.invoice-facts {
  display: grid; grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 12px 32px; margin: 0 0 28px;
  border-block: var(--rule); padding-block: 20px;
}
.invoice-facts dt { margin-bottom: 2px; }
.invoice-facts dd { margin: 0; font-size: 14px; }

/* 390px is the check that has caught every sideways-scroll bug here. */
@media (max-width: 620px) {
  .invoice-sheet { padding: 24px; }
  .invoice-parties, .invoice-facts { grid-template-columns: minmax(0, 1fr); }
}

@media print {
  /* The chrome is navigation. On paper it is ink. */
  .nav, .footer, .searchband, .no-print { display: none !important; }
  .invoice-sheet { border: 0; padding: 0; max-width: none; }
  .wrap { padding-block: 0; }
  a { text-decoration: none; color: inherit; }
}

/* — reviews — */
/* auto-fill with a minmax floor rather than a fixed column count. A hard
   grid-template-columns with no breakpoint is what has caused every
   sideways-scroll bug on this site so far; the min() inside the floor is what
   stops a 280px track from being wider than a 390px phone's gutter allows. */
/* A section heading with the way to the whole of it beside it: baseline
   aligned so the small link sits on the heading's baseline, wrapping rather
   than crowding on a narrow screen. */
.section-head {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  gap: 16px;
  flex-wrap: wrap;
  margin-bottom: 20px;
}

.review-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(min(280px, 100%), 1fr));
  gap: 32px;
  /* Reviews are wildly different heights once one carries a portrait
     photograph of a wall. Without this they stretch to the tallest in the row
     and the short ones float in the middle of a lot of nothing. */
  align-items: start;
}

.review {
  border-top: var(--rule);
  padding-top: 18px;
  min-width: 0;
}
.review-head { display: flex; align-items: baseline; gap: 12px; flex-wrap: wrap; }
.review-by {
  font-family: var(--font-heading);
  font-weight: 800;
  font-size: 14px;
  margin: 0;
}
.review-by .text-muted { font-family: var(--font-body); font-weight: 400; }
.review-piece { font-size: 13px; margin: 8px 0 0; }
.review-body {
  margin: 10px 0 0;
  font-size: 15px;
  line-height: 25px;
  max-width: 52ch;
  /* Customer text, so it may be one unbroken string. Without this a pasted URL
     widens the grid track and the page scrolls sideways. */
  overflow-wrap: anywhere;
}
.review-photo { margin: 16px 0 0; max-width: 360px; }
.review-photo img { width: 100%; height: auto; }

/* The stars are drawn from the accent, which is the one red on the site and is
   used sparingly. Empty ones are the divider so a 3 of 5 reads as three. */
.stars { display: inline-flex; gap: 2px; line-height: 1; flex: none; }
.star { font-size: 15px; color: var(--color-divider); }
.star.is-filled { color: var(--color-accent-ink); }

.review-form { margin-top: 44px; padding-top: 24px; max-width: 560px; }

.rating-field { border: 0; padding: 0; margin: 0 0 14px; }
.rating-field legend { padding: 0; margin-bottom: 8px; }
.rating-row { display: flex; gap: 8px; flex-wrap: wrap; }
.rating-pick {
  position: relative;
  display: inline-flex;
  align-items: center;
  justify-content: flex-start;
  min-width: 44px;
  padding: 9px 14px;
  cursor: pointer;
  font-family: var(--font-heading);
  font-weight: 800;
  font-size: 14px;
  background: var(--control-bg);
  border: 1px solid var(--color-divider);
}
.rating-pick input { position: absolute; opacity: 0; width: 0; height: 0; }
.rating-pick:hover { border-color: var(--color-text); }
.rating-pick:has(input:checked) {
  border-color: var(--color-accent-ink);
  color: var(--color-accent-ink);
}
.rating-pick:has(input:focus-visible) { outline: 2px solid var(--color-accent-ink); outline-offset: 2px; }

/* — admin: the moderation queue — */
.admin-review { display: grid; grid-template-columns: minmax(0, 1fr) 220px; gap: 24px; align-items: start; }
.admin-review-photo { margin: 0; }
.admin-review-photo img { width: 100%; height: auto; }
.admin-review-actions {
  display: flex;
  gap: 10px;
  flex-wrap: wrap;
  align-items: center;
  margin-top: 18px;
  padding-top: 14px;
  border-top: 1px solid var(--color-divider);
}
@media (max-width: 700px) {
  .admin-review { grid-template-columns: 1fr; gap: 16px; }
  .admin-review-photo { max-width: 260px; }
}

/* — the price table — */
/* A wide table on a narrow screen is the classic sideways-scroll bug in this
   project. The table scrolls inside its own box; the page never does. */
.table-scroll { overflow-x: auto; }

/* A price list is read down one column at a time, so it is given a measure
   rather than the whole page. Stretched to a 1200px gutter the eye has to
   travel a hand's width from a size to its price, which is the same reason
   running text is not set full bleed either. */
.price-table {
  width: 100%;
  max-width: 620px;
  min-width: 340px;
  border-collapse: collapse;
  font-size: 15px;
  /* Digits line up under each other, which is the whole point of a price
     column: 2,650 and 5,200 are comparable at a glance only if they are. */
  font-variant-numeric: tabular-nums;
}
.price-table th, .price-table td {
  text-align: left;
  padding: 14px 24px 14px 0;
  border-bottom: 1px solid var(--color-divider);
  vertical-align: baseline;
}
.price-table thead th {
  font-family: var(--font-heading);
  font-weight: 800;
  font-size: 11px;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  padding-bottom: 10px;
  border-bottom: var(--rule);
}
.price-table tbody th { font-family: var(--font-heading); font-weight: 800; white-space: nowrap; }
.price-table tbody tr:last-child th,
.price-table tbody tr:last-child td { border-bottom: var(--rule); }

/* The size names the row, so it holds the left edge like every other label on
   the site. The dimensions are reference rather than the point, so they sit
   quieter and take the slack when the table is wide. */
.price-table th:first-child { width: 1%; }
.price-table td:nth-child(2) { width: auto; color: var(--color-neutral-700); }
.price-table .num { text-align: right; white-space: nowrap; padding-right: 0; width: 1%; }

/* The digital file is not a size, it is the photograph on its own, so it is
   set off from the ladder rather than sitting at the top of it as if it were
   the smallest print. */
.price-table tbody .price-row-set-off th,
.price-table tbody .price-row-set-off td { border-bottom: var(--rule); }

@media (max-width: 560px) {
  .price-table { font-size: 14px; }
  .price-table th, .price-table td { padding: 12px 14px 12px 0; }
}

/* — where to buy a frame — */
/* A plain list. These are links off the site, so they read as references
   rather than as controls. */
.supplier-list { list-style: none; margin: 0; padding: 0; }
.supplier-list li {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 8px;
  padding: 6px 0;
  border-bottom: 1px solid var(--color-divider);
  font-size: 14px;
}

/* — rooms: photographs on a wall — */
/* A wall is drawn in percent of its own size, so a print is the fraction of the
   wall it would be in a room. The picture and the blueprint are the same
   layout: the picture shows the photographs, the blueprint shows outlines and
   the measurements to hang them by. Neither composites anything. */
.room-figure { max-width: 980px; }
/* The wall and the list of rooms side by side, so another room is a click
   from the one in view. Below 900px the list lies above the wall as a strip. */
.room-showcase { display: grid; grid-template-columns: minmax(0, 980px) 200px; gap: 24px; align-items: start; }
.room-rail { min-width: 0; }
.room-rail .label { display: block; margin: 0 0 8px; }
.room-rail ul { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 8px; }
.room-rail .choice { display: block; }
@media (max-width: 900px) {
  .room-showcase { grid-template-columns: minmax(0, 1fr); }
  .room-rail { order: -1; }
  .room-rail ul { flex-direction: row; overflow-x: auto; scroll-snap-type: x proximity; padding-bottom: 4px; }
  .room-rail li { flex: none; scroll-snap-align: start; }
}
/* A change of count or room glides: pieces that stay move to their new
   place. Browsers without view transitions simply swap. */
@media (prefers-reduced-motion: reduce) {
  ::view-transition-group(*), ::view-transition-old(*), ::view-transition-new(*) { animation: none !important; }
}
.room-preview-figure { max-width: 860px; }
.room-preview-wrap { margin-bottom: 44px; }

.room-wall {
  position: relative;
  width: 100%;
  overflow: hidden;
}
/* The floor, so a wall reads as a wall and not as a rectangle. */
.room-wall::after {
  content: "";
  position: absolute;
  inset: auto 0 0 0;
  height: 0;
  border-top: 2px solid rgba(32, 30, 29, 0.28);
}

.room-print {
  position: absolute;
  box-shadow: 0 1px 3px rgba(32, 30, 29, 0.35);
}
/* A piece is built as the product page's preview is (.frame-moulding,
   .frame-mat): the moulding is padding in the frame's colour, the mat is
   padding on paper inside it, and the photograph fills what is left at its
   own proportions (rooms.fitted), so every border is even on all four sides.
   Each padding is the real width against the piece (hangItem.Moulding, .Mat):
   a heavy frame on a small print reads heavy. */
.room-moulding {
  position: absolute;
  inset: 0;
  box-sizing: border-box;
}
.room-paper { width: 100%; height: 100%; box-sizing: border-box; }
.room-image { position: relative; width: 100%; height: 100%; }
/* The frame is the edge of the moulding; a white frame on a pale wall needs
   the hairline to have an edge at all. The piece's shadow is the frame's. */
.showcase { padding-block: 32px 56px; }
.showcase-title { font-size: clamp(32px, 4.5vw, 56px); line-height: 1.05; margin: 0 0 12px; }
.showcase-lede { max-width: 56ch; margin: 0 0 24px; }
.room-print-link { position: absolute; inset: 0; z-index: 2; }
.room-print-link:focus-visible { outline: 2px solid var(--color-accent-ink); outline-offset: 2px; }
.room-print.is-framed { box-shadow: 0 2px 6px rgba(32, 30, 29, 0.35); }
.room-print.is-framed .room-moulding { box-shadow: inset 0 0 0 1px rgba(32, 30, 29, 0.18); }
/* The mat, the rebate where it meets the moulding, and the bevel cut around
   the opening, as on the product page. */
.room-paper.has-mat { background: var(--paper); box-shadow: inset 0 0 0 1px rgba(32, 30, 29, 0.18); }
.room-paper.has-mat .room-image { box-shadow: 0 0 0 1px rgba(32, 30, 29, 0.28); }
/* The photograph and its opening have the same proportions, so cover crops
   nothing but a rounding pixel. product_image sets an inline max-width and
   aspect-ratio for the masonry grid, which is why these need !important. */
.room-print .product-photo,
.room-print .image-slot {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100% !important;
  max-width: none !important;
  aspect-ratio: auto !important;
  object-fit: cover;
}

/* The room around the wall: furniture in elevation, drawn in wall millimetres
   so it is to the same scale as the prints (rooms.Furniture). Behind the
   prints in the document, though nothing in it ever reaches one. */
.room-furniture {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  pointer-events: none;
}
.room-furniture rect { stroke: var(--room-edge); stroke-width: 1; vector-effect: non-scaling-stroke; }
.rf-fabric { fill: var(--room-fabric); }
.rf-cushion { fill: var(--room-cushion); }
.rf-wood { fill: var(--room-wood); }
.rf-wood-dark { fill: var(--room-wood-dark); }
.rf-metal { fill: var(--room-metal); }
.rf-linen { fill: var(--room-linen); }
.rf-shade { fill: var(--room-shade); }
.rf-ceramic { fill: var(--room-ceramic); }
.rf-skirting { fill: var(--room-skirting); }
.rf-leaf { fill: var(--room-leaf); }
.rf-leaf-dark { fill: var(--room-leaf-dark); }
.rf-screen { fill: var(--room-screen); }
.rf-glass { fill: var(--room-glass); }
.rf-play { fill: var(--room-play); }
.rf-play-cool { fill: var(--room-play-cool); }
.rf-book { fill: var(--room-book); }
.rf-book-dark { fill: var(--room-book-dark); }

/* The blueprint drops the wall's colour for the page's, so every stroke and
   label is drawn in the theme's own tokens and reads in both themes. */
.room-wall.is-blueprint {
  background: var(--color-bg) !important;
  border: var(--rule);
}
.room-wall.is-blueprint::after { border-top-color: var(--color-divider); }
.room-wall.is-blueprint .room-print {
  background: transparent;
  box-shadow: none;
  outline: 2px solid var(--color-accent-ink);
  outline-offset: -2px;
}
/* In the blueprint the room is outlines in the text colour, faint, so the
   prints stay the subject; the piece's outside edge is solid, and the
   photograph inside a frame or a mat is dashed. The moulding's colour is inline, hence
   !important. */
.room-wall.is-blueprint .room-furniture rect {
  fill: none;
  stroke: color-mix(in srgb, var(--color-text) 35%, transparent);
}
.room-wall.is-blueprint .room-moulding { background: transparent !important; box-shadow: none; }
.room-wall.is-blueprint .room-paper { background: transparent; box-shadow: none; }
.room-wall.is-blueprint .room-print.is-framed .room-image,
.room-wall.is-blueprint .room-paper.has-mat .room-image {
  box-shadow: none;
  outline: 1px dashed var(--color-accent-ink);
}
.room-dim {
  position: absolute;
  inset: 6px 0 auto 6px;
  font-size: 11px;
  line-height: 14px;
  letter-spacing: 0.04em;
  color: var(--color-accent-ink);
  font-variant-numeric: tabular-nums;
}

.room-centre, .room-span, .room-gap, .room-anchor {
  position: absolute;
  height: 0;
  pointer-events: none;
}
.room-centre span, .room-span span, .room-gap span, .room-anchor span {
  position: absolute;
  left: 0;
  bottom: 3px;
  white-space: nowrap;
  font-size: 10px;
  line-height: 12px;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--color-text);
  font-variant-numeric: tabular-nums;
}
.room-centre { left: 0; right: 0; border-top: 1px dashed var(--color-accent-ink); }
.room-centre span { left: auto; right: 6px; color: var(--color-accent-ink); }
.room-span { border-top: 2px solid var(--color-text); }
.room-gap { border-top: 1px solid var(--color-accent-ink); }
.room-gap span { color: var(--color-accent-ink); left: 50%; transform: translateX(-50%); bottom: 4px; }
.room-anchor { bottom: 10px; border-top: 2px solid var(--color-text); }

/* The photograph of the room with the layout drawn onto it in perspective. The
   stage is laid out in the backdrop's own pixels and the plane in wall
   millimetres, and app.js scales the stage to its frame. Hidden until it has,
   so a visitor without JavaScript sees the elevation instead. */
.room-stage-frame { position: relative; width: 100%; overflow: hidden; }
.room-stage { position: absolute; left: 0; top: 0; transform-origin: 0 0; transform: scale(var(--k, 1)); }
.room-backdrop { position: absolute; inset: 0; }
.room-backdrop .product-photo,
.room-backdrop .image-slot {
  width: 100%;
  height: 100% !important;
  max-width: none !important;
  aspect-ratio: auto !important;
  object-fit: cover;
}
.room-plane { position: absolute; left: 0; top: 0; transform-origin: 0 0; }

.room-caption { font-size: 13px; line-height: 20px; margin: 14px 0 0; max-width: 72ch; }
.room-views { display: flex; flex-wrap: wrap; gap: 12px; margin-bottom: 24px; }
.room-frames { display: flex; flex-wrap: wrap; align-items: center; gap: 8px 12px; margin: -8px 0 24px; }
.room-frames .label { margin: 0 4px 0 0; }
.room-count { display: flex; flex-wrap: wrap; align-items: center; gap: 8px 12px; margin: -8px 0 24px; }
.room-count .label { margin: 0 4px 0 0; }
.room-count input[type="range"] { width: min(280px, 100%); accent-color: var(--color-text); }
.room-fewer { font-size: 13px; margin: -12px 0 20px; }
.room-count output { min-width: 2ch; font-variant-numeric: tabular-nums; font-weight: 800; }
.room-buy { display: flex; flex-wrap: wrap; gap: 12px; }
.room-plan { max-width: none; }
.wall-links { margin: 0 0 24px; }
.wall-links .label { display: block; margin: 0 0 10px; }
.wall-links ul { list-style: none; margin: 0; padding: 0; display: flex; flex-wrap: wrap; gap: 8px 12px; }

/* The admin's context tab: a photograph beside what is said about it. */
.context-row { display: grid; grid-template-columns: 160px 1fr; gap: 20px; align-items: start; }
.context-thumb img { width: 100%; height: auto; }
.context-themes { border: 0; padding: 0; margin: 12px 0 0; display: flex; flex-wrap: wrap; gap: 6px 16px; }
@media (max-width: 560px) { .context-row { grid-template-columns: 1fr; } }

.room-list {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(min(360px, 100%), 1fr));
  gap: 48px 32px;
}
.room-card-figure { display: block; }
.room-card-title { font-size: 20px; margin: 14px 0 4px; }
.room-card-title a { color: inherit; }

@media (max-width: 560px) {
  .room-dim { font-size: 9px; line-height: 11px; inset: 3px 0 auto 3px; }
  .room-centre span, .room-span span, .room-gap span, .room-anchor span { font-size: 8px; }
}

/* The admin's corner marker: the photograph of a room with numbered dots on the
   wall's corners, clicked in. Drawn in the accent stroke on the page ground, as
   every other stroke is. */
.corner-stage { max-width: 760px; }
.corner-stage .product-photo { width: 100%; height: auto; }
.corner-dot {
  position: absolute;
  width: 22px;
  height: 22px;
  margin: -11px 0 0 -11px;
  display: grid;
  place-items: center;
  font-size: 11px;
  font-weight: 800;
  background: var(--color-accent);
  color: var(--paper);
  pointer-events: none;
}

/* A photograph's Genesis record (JOURNAL/0064). A framed panel rather than
   another ruled section, so it reads as a record. Ink, not the accent: it
   asks the reader for nothing. */
.genesis-record {
  margin-top: 32px;
  padding: 20px;
  border: 2px solid var(--color-text);
  background: var(--color-surface);
}
.genesis-logo { display: block; width: 160px; height: auto; margin-bottom: 16px; }
.genesis-logo-white { display: none; }
html[data-theme="dark"] .genesis-logo-ink { display: none; }
html[data-theme="dark"] .genesis-logo-white { display: block; }
html[data-theme="dark"] .genesis-mark.genesis-logo-white { display: inline-block; }
.genesis-record-title {
  margin: 0;
  font-family: var(--font-heading);
  font-weight: var(--font-heading-weight);
  font-size: 22px;
  line-height: 1.2;
}
.genesis-record-date { margin: 6px 0 12px; }
.genesis-record-note { margin: 0 0 16px; font-size: 15px; line-height: 1.5; }
.genesis-record-actions { display: flex; flex-wrap: wrap; gap: 8px; }
/* The same record, said at the top beside the title, and a way down to it. */
.genesis-badge {
  display: inline-flex;
  align-items: center;
  gap: 10px;
  margin-top: 16px;
  padding: 8px 12px;
  border: 2px solid var(--color-text);
  color: var(--color-text);
  text-decoration: none;
  font-family: var(--font-heading);
  font-weight: var(--font-heading-weight);
  font-size: 14px;
}
.genesis-badge:hover { background: color-mix(in srgb, var(--color-text) 7%, transparent); }
.genesis-mark { width: 24px; height: 24px; }
.genesis-badge-meaning { margin: 8px 0 0; max-width: 46ch; font-size: 13px; line-height: 1.45; color: color-mix(in srgb, var(--color-text) 70%, transparent); }
