/* The site is the same paper the journal page is written on, so the page does
   not arrive looking like it came from somewhere else. */
:root {
  --paper: #f7f3ea;
  --ink: #26221d;
  --quiet: #6b6152;
  /* Dates, prices and quiet labels. #9a8a6d was the original and
     measured 3.0:1 on paper, which fails WCAG AA for anything under
     24px -- and every use of this is 14 or 15px. This is the same hue
     and saturation, dark enough to pass on paper (5.07), on the
     paper-deep squares (4.64) and on a white product tile (5.61).
     tests/test_contrast.py measures it rather than trusting it. */
  --faint: #73664f;
  --rule: rgba(120, 104, 78, 0.28);
  --serif: "Iowan Old Style", "Palatino Linotype", Palatino, Georgia, serif;

  /* One spacing scale. Every margin below comes from here, so things line
     up by construction instead of by each block guessing a number. */
  --s1: 8px;
  --s2: 14px;
  --s3: 22px;
  --s4: 34px;
  --s5: 52px;

  /* One measure for the controls on the result screen, so the square, the
     field and the buttons share a left and a right edge. */
  /* The page. 1080px because the journal page is drawn at 1080 and is also
     the file people share, so on screen it is the size it will be posted at
     rather than scaled to 63% of itself, which is what 680 was doing to it.
     680 was chosen when this was a conversation and nothing else, and was
     never revisited once it started showing pictures. */
  --page: 1080px;

  /* Prose, and the controls that sit in it: about 75 characters at 19px.
     A measure for reading is a different thing from a page width, and text
     dragged to 1080px is text nobody finishes a line of. */
  --read: 34em;
}
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
  background: var(--paper); color: var(--ink);
  font: 19px/1.65 var(--serif);
  min-height: 100vh; display: flex; flex-direction: column;
  padding: 0 20px;
}
.sr { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); }

/* --- the way around the site ------------------------------------------- */
/* Built to take more than it holds. The journal was reachable only through
   a line in the footer, which is where links go to be missed.

   No hamburger. Three words wrap onto a second line on a phone and are
   still readable; a menu that has to be opened to be read is worse than
   one that wraps. */
.sitenav {
  width: 100%; max-width: var(--page); margin: 0 auto;
  padding: var(--s3) 0;
  display: flex; flex-wrap: wrap; align-items: baseline;
  gap: var(--s2) var(--s4);
  border-bottom: 1px solid var(--rule);
}
.wordmark {
  font-size: 21px; color: var(--ink); text-decoration: none;
  letter-spacing: 0.01em;
}
.sitenav nav {
  display: flex; flex-wrap: wrap; gap: var(--s4);
  margin-left: auto;
}
.sitenav nav a {
  font-size: 17px; color: var(--quiet); text-decoration: none;
  padding-bottom: 2px;
}
.sitenav nav a:hover { color: var(--ink); }
/* The page you are on, said quietly rather than with a slab of color. */
.sitenav nav a[aria-current="page"] {
  color: var(--ink);
  border-bottom: 1px solid var(--ink);
}
@media (max-width: 480px) {
  .sitenav { gap: var(--s1) var(--s3); }
  .sitenav nav { margin-left: 0; width: 100%; }
}

#app { flex: 1; width: 100%; max-width: var(--page); margin: 0 auto; padding: var(--s5) 0 40px; }
/* Hidden means hidden. `.stage[hidden]` is a class plus an attribute, and
   `#intro` is an id, so giving the front door `display: grid` by its id beat
   it and the section stayed on screen underneath the conversation. Any rule
   that sets display on a section would have done the same. */
[hidden] { display: none !important; }

/* --- intro --- */
/* Every section is the width of the page, and everything in it starts at the
   same left edge. Nothing gets a column of its own and nothing is centered:
   that is what had the screens scattered, each one left at whatever width
   its own last fix happened to give it.

   Paragraphs keep a maximum line length, because that is typography rather
   than layout. They still begin at the left edge with everything else. */
#intro {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 420px);
  gap: var(--s5);
  align-items: start;
  text-align: left;
}
#intro .welcome { grid-column: 1; grid-row: 1; }
#intro .scene { grid-column: 2; grid-row: 1; }
#intro .bird { margin-bottom: 14px; }

/* Stacked when there is no room for two, saying first: it is the reason to
   have come back. */
@media (max-width: 860px) {
  #intro { grid-template-columns: 1fr; gap: var(--s4); }
  #intro .scene { grid-column: 1; grid-row: 1; }
  #intro .welcome { grid-column: 1; grid-row: 2; }
}
h1 { font-size: 46px; font-weight: normal; letter-spacing: 0.4px; margin-bottom: 22px; }
.lede { color: var(--quiet); margin: 0 0 18px; max-width: var(--read); }
.fineprint { color: var(--faint); font-size: 15px; margin-top: 22px; }

button { font-family: var(--serif); cursor: pointer; }
.primary {
  display: inline-block;
  background: var(--ink); color: var(--paper); border: 0;
  font-size: 19px; padding: 13px 30px; border-radius: 3px;
  margin-top: 14px;
}
.primary:hover { background: #3b352c; }
.primary:disabled { opacity: 0.45; cursor: default; }

/* The example openings, above the box they are an alternative to. Quieter
   than the composer, because they are a way in for somebody who does not
   know what to type and not a second thing being asked of them.

   Two by two. Willy: "they should be two on a row and two rows, not 4 rows
   with one per row." Equal columns rather than pills, so four sentences of
   different lengths still line up, and both buttons in a row take the
   height of the taller -- a one-line button beside a two-line one reads as
   a mistake rather than as a pair. */
.openers { margin: var(--s3) 0 0; text-align: left; }
.openerlede { color: var(--faint); font-size: 15px; margin: 0 0 10px; }
.openerlist {
  display: grid; grid-template-columns: 1fr 1fr;
  gap: 8px; align-items: stretch;
}
.opener {
  background: none; color: var(--quiet);
  border: 1px solid var(--rule); border-radius: 14px;
  font: 16px/1.35 var(--serif); text-align: left;
  padding: 10px 14px; width: 100%;
}
.opener:hover { color: var(--ink); border-color: var(--faint); background: #efe9dc; }
/* Asking for the shop is a decision, so it looks like one -- but it is not
   the loudest thing on the screen, because the square is what they came for. */
/* --- the shop --- */
/* A section with a left edge, like the rest of the world's websites. */
#shop { text-align: left; }
#shop .objects { margin-top: var(--s4); }
#shop .after { margin-top: var(--s5); text-align: left; }
#shop .fineprint { text-align: left; }
.shopline {
  font-size: 26px; line-height: 1.35; max-width: var(--read);
  margin: 0 0 var(--s1);
}
/* One line of instruction, not prose, so it takes the column rather than
   the reading measure. At 34em it was about half the width of the page
   and broke across two lines for no reason. Willy, 10 October 2026: "it
   is only half with width. it should spand the entire width fo it does
   not have to wrap to two lines." */
.shopnote {
  color: var(--quiet); font-size: 15px;
  margin: 0;
}

.quiet {
  background: none; border: 0; color: var(--faint);
  font-size: 16px; text-decoration: underline; padding: 8px;
}

/* --- conversation --- */
/* The same shape as the front door: the words on the left, the picture on
   the right. Everything here was capped at the reading measure, so two
   fifths of the page sat empty beside it. */
#talk {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 420px);
  gap: var(--s3) var(--s5);
  align-items: start;
}
#talk > * { grid-column: 1; }
#talk .listener {
  grid-column: 2; grid-row: 1 / span 4;
  max-width: none; margin: 0;
  /* He stays beside the conversation as it grows. Without this his column
     ends where the picture does, and everything below it is a half-width
     thread with dead space to its right -- which is what the bottom of the
     screen looked like while the page was being written. */
  position: sticky; top: var(--s4);
}

@media (max-width: 860px) {
  #talk { grid-template-columns: 1fr; }
  #talk .listener {
    grid-column: 1; grid-row: 1; max-width: 320px;
    /* And not sticky. Staying beside the conversation is the point of him in
       two columns; in one he pins to the top of the screen with nothing
       behind him, and the thread scrolls through the gaps in the branch.
       Willy: "scrolling goes behind the image of him on the branch and text
       shows through except for him and the branches. It looks funky." */
    position: static;
  }
}

.thread { margin-bottom: 30px; }
.said { margin-bottom: 26px; max-width: var(--read); }
.said .who {
  font-size: 13px; letter-spacing: 2.6px; text-transform: uppercase;
  color: var(--faint); display: block; margin-bottom: 5px;
}
.said.alfred p { font-size: 25px; line-height: 1.5; }
.said.you p { color: var(--quiet); font-style: italic; }
/* The shorter wait, between questions. Same reasoning as Switch below: a
   mark that does not move reads as a page that has stopped. */
.said.thinking p {
  color: var(--faint); font-style: italic;
  animation: breathing 1.4s ease-in-out infinite;
}
@keyframes breathing {
  0%, 100% { opacity: 0.35; }
  50%      { opacity: 1; }
}
@media (prefers-reduced-motion: reduce) {
  .said.thinking p { animation: none; opacity: 0.7; }
}

/* The same measure as every other field on the site. A textarea stretched
   to 1080px is a textarea nobody can read their own sentence in. */
#talk .composer { display: flex; gap: 12px; align-items: flex-end; max-width: var(--read); }
textarea {
  flex: 1; font: 19px/1.55 var(--serif); color: var(--ink);
  background: rgba(255,255,255,0.5); border: 1px solid var(--rule);
  border-radius: 3px; padding: 11px 13px; resize: none;
}
textarea:focus { outline: 2px solid var(--quiet); outline-offset: -1px; }
#talk .composer .primary { margin-top: 0; white-space: nowrap; }

/* --- the page --- */
.paper-frame {
  /* The journal page is a fixed 1080x1350 artifact -- the same one that gets
     shared -- so it is shown at its own proportions and scaled, never reflowed.
     What is on screen is then exactly what a screenshot would capture. */
  width: 100%; aspect-ratio: 1080 / 1350;
  border: 1px solid var(--rule); box-shadow: 0 2px 22px rgba(80, 66, 42, 0.13);
  overflow: hidden;
}
#page { width: 1080px; height: 1350px; border: 0; transform-origin: 0 0; display: block; }

/* A phone. The page is no longer a fixed-ratio artifact being shrunk to fit;
   it is laid out for the screen and is as tall as what Alfred wrote, so the
   frame cannot hold it to 1080:1350 or clip it. */
.paper-frame.reflowed { aspect-ratio: auto; overflow: visible; }

.after { margin-top: var(--s4); }
.sitefoot {
  max-width: var(--page); margin: 0 auto; width: 100%;
  padding: 26px 0 30px; border-top: 1px solid var(--rule);
  color: var(--faint); font-size: 15px;
}
/* The copyright line kept the flex the footer used to have, now that it is
   one row inside the footer rather than the whole of it. */
.footrow {
  display: flex; justify-content: space-between; gap: 16px; flex-wrap: wrap;
}
/* Alfred on himself, on every page. Fine print, and set as fine print:
   one column the full width of the footer, smaller and fainter than the page
   above it. Willy: "this is referred to as the fine print for a reason."

   It was briefly two columns at the reading measure, which is what prose
   gets everywhere else on this site. That is the rule for something somebody
   is meant to sit and read; a disclaimer is something they should be able to
   find and check, which is a different job and looks different. */
.alfredsays { margin-bottom: var(--s4); }
.alfredsays p { font-size: 14px; line-height: 1.6; }
.error { color: #8a3a2a; margin-top: 14px; }

@media (max-width: 640px) {
  #app { padding-top: 40px; }
  h1 { font-size: 36px; }
  body { font-size: 18px; }
  .said.alfred p { font-size: 22px; }
  #talk .composer { flex-direction: column; align-items: stretch; }
}

/* --- keeping a line ---------------------------------------------------
   One column, and everything in it full width. The square, the field, the
   buttons and the box at the bottom share a left and a right edge; before
   this each one centered itself at its own width and nothing lined up with
   anything. */
/* One left edge for the whole screen. Centring the column under a
   left-aligned instruction gave the page two of them. */
.keepit {
  width: 100%;
  margin: var(--s4) 0 0;
  text-align: left;
}
.keepit > * + * { margin-top: var(--s3); }

.yours { color: var(--quiet); font-size: 15px; }
/* One, two or three, side by side, shrinking to share the width rather than
   wrapping or overflowing. They are square, so three at a third of the
   column is still legible. */
.squares { display: flex; gap: var(--s1); align-items: flex-start; }
.squares .square {
  /* Each one wants 420 and shrinks only when three have to share the page.
     At `1 1 0` a single square stretched across the whole 1080 and dwarfed
     everything under it. */
  display: block; min-width: 0; flex: 0 1 420px;
  height: auto;
  border-radius: 6px;
  box-shadow: 0 1px 14px rgba(80, 66, 42, 0.12);
}
.squares.working { opacity: 0.45; }

.ded { display: block; }
.ded span {
  display: block; font-size: 15px; color: var(--quiet);
  margin-bottom: var(--s1);
}
.field {
  width: 100%; font: 17px/1.5 var(--serif); color: var(--ink);
  background: rgba(255, 255, 255, 0.55); border: 1px solid var(--rule);
  border-radius: 3px; padding: 11px 13px;
}
.field:focus { outline: 2px solid var(--quiet); outline-offset: -1px; }

/* --- sharing --- */
.share-row { display: flex; gap: var(--s1); flex-wrap: wrap; }
.share-row button, .share-row a {
  font: 16px/1 var(--serif); color: var(--ink); cursor: pointer;
  background: rgba(255, 255, 255, 0.45); border: 1px solid var(--rule);
  border-radius: 3px; padding: 13px 14px; text-align: center;
  text-decoration: none;
}
.share-row button:hover, .share-row a:hover {
  background: #fff; border-color: var(--quiet);
}
.share-note { color: var(--faint); font-size: 14px; }

/* Asking for the shop is a decision, so it looks like one -- but it is not
   the loudest thing on the screen, because the square is what they came for. */
.secondary {
  display: inline-block;
  background: none; color: var(--ink); border: 1px solid var(--rule);
  font: 17px/1 var(--serif); padding: 14px 20px; border-radius: 3px;
  margin-top: var(--s4);
}
.secondary:hover { border-color: var(--quiet); background: #fff; }

/* One box rather than two buttons. Label, field, button -- stacked, each
   the width of the column. */
.again {
  width: 100%;
  margin: var(--s5) 0 0;
  padding-top: var(--s4);
  border-top: 1px solid var(--rule);
}
.again label {
  display: block; color: var(--quiet); font-size: 15px;
  margin-bottom: var(--s1);
}
.again textarea { width: 100%; }
.again .primary { display: inline-block; margin-top: var(--s2); }

/* The objects, drawn rather than listed. Someone choosing between three
   pictures of their own line is choosing; someone reading "Framed print,
   $22-65" is shopping, and a shop is what Alfred exists instead of. */
/* A grid, left aligned, with the cards starting at the top.
   Willy: "images should be top-aligned not middle aligned ... there are no
   sections so everything on a page is center aligned ... In the real world
   centering is for greeting cards, not websites."

   A flex row with justify-content: center put the last row of two in the
   middle of the page, and stretched the cards to a common height, so the one
   with a two-line name sat higher than the rest and no two prices agreed. */
/* #objects is a plain column of sections. Only the row inside a section is
   a grid -- as a grid itself it made each section a column and stacked that
   line's five products down it. */
.objects { display: block; margin-top: var(--s3); }

/* One section per chosen line, each with its own row of products. */
.forline + .forline { margin-top: var(--s5); }
.forline h3 {
  font-size: 19px; font-weight: normal; line-height: 1.35;
  color: var(--ink); max-width: var(--read); margin-bottom: var(--s2);
}
.objects-row {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(164px, 1fr));
  gap: var(--s4) var(--s3);
  align-items: start;
  margin-top: var(--s3);
}
.object {
  display: block;
  background: none; border: 0; padding: 0; cursor: pointer;
  text-align: left; font-family: var(--serif);
  position: relative;
}

/* One shape for every picture, whatever the product's own proportions, so
   the tops line up and the names below them start on the same line. */
.object .shot {
  display: block; width: 100%; aspect-ratio: 1;
  cursor: zoom-in;
  background: #fff;
  border: 1px solid var(--rule); border-radius: 2px;
  box-shadow: 0 1px 10px rgba(80, 66, 42, 0.10);
  transition: box-shadow 120ms, border-color 120ms;
  overflow: hidden;
}
.object img { display: block; width: 100%; height: 100%; object-fit: contain; }
.object:hover .shot { border-color: var(--quiet); box-shadow: 0 2px 16px rgba(80, 66, 42, 0.2); }

/* Two lines of room for the name, so a long one does not push its own price
   out of line with the rest of the row. */
.object .name {
  display: block; font-size: 16px; line-height: 1.3;
  margin-top: var(--s1); color: var(--ink);
  min-height: 2.6em;
}
.object .price { display: block; font-size: 14px; color: var(--faint); }

/* While they are being drawn. Every card is a render on the server, so the
   wait is real, and an empty box for several seconds reads as a page that
   has stopped working. */
.objects.working { pointer-events: none; }
.ghost .shot {
  background:
    linear-gradient(100deg, rgba(255,255,255,0) 30%, rgba(255,255,255,0.7) 50%,
                    rgba(255,255,255,0) 70%) #ece5d8;
  background-size: 300% 100%;
  animation: sweep 1.3s ease-in-out infinite;
}
.ghost .name, .ghost .price {
  background: rgba(120, 104, 78, 0.14); border-radius: 2px; color: transparent;
}
.ghost .name { min-height: 1.3em; width: 78%; }
.ghost .price { width: 42%; margin-top: 5px; }
@keyframes sweep {
  from { background-position: 100% 0; }
  to   { background-position: 0 0; }
}
@media (prefers-reduced-motion: reduce) { .ghost .shot { animation: none; } }

/* --- a larger look --- */
.object { position: relative; }
.object .bigger {
  position: absolute; top: 7px; right: 7px;
  width: 30px; height: 30px; border-radius: 50%;
  background: rgba(247, 243, 234, 0.9); border: 1px solid var(--rule);
  color: var(--quiet); font: 15px/1 var(--serif);
  display: flex; align-items: center; justify-content: center;
  opacity: 0; transition: opacity 120ms;
  pointer-events: none;
}
.object:hover .bigger, .object:focus .bigger { opacity: 1; }
@media (hover: none) { .object .bigger { opacity: 1; } }

.zoom {
  position: fixed; inset: 0; z-index: 50;
  background: rgba(30, 25, 18, 0.88);
  /* Scrolls, and starts at the top. Centring a card taller than the window
     put the size and the Add to cart button below the bottom edge with no
     way to reach them. Willy: "the size button and Add to Cart button are
     cut off at the bottom of the modal." */
  display: flex; align-items: flex-start; justify-content: center;
  padding: 28px 20px;
  overflow-y: auto; overscroll-behavior: contain;
}
.zoom[hidden] { display: none; }

/* A card on the site's own paper rather than controls floating on a dark
   overlay. The buttons here are the buttons everywhere else, which they
   could not be while the ground behind them was nearly black. */
.zoomframe {
  margin: 0 auto; max-width: min(92vw, 560px);
  background: var(--paper); border-radius: 4px;
  box-shadow: 0 6px 40px rgba(0, 0, 0, 0.45);
  overflow: hidden;
}
.zoomframe img {
  width: 100%; max-height: 54vh; object-fit: contain;
  display: block; background: #efe9dc;
}
@media (max-width: 640px) {
  /* Smaller, so the size, the price and the button are on the screen at the
     same time as the thing they refer to. At 54vh the photograph pushed the
     confirmation below the bottom of the modal: the button vanished and the
     reply was somewhere nobody could see. */
  .zoomframe img { max-height: 38vh; }
}
/* The preview, standing in while the full render is fetched. Dimmed rather
   than hidden: something true and slightly faded reads as loading, where an
   empty box reads as broken. */
.zoomframe img.loading { min-height: 300px; opacity: 0.55; }
.zoomframe figcaption {
  color: var(--quiet); font: 16px/1.5 var(--serif);
  padding: var(--s3) var(--s3) 0;
}
.zoomclose {
  position: absolute; top: 14px; right: 18px;
  background: none; border: 0; color: #e6dfd2;
  font: 34px/1 var(--serif); cursor: pointer; padding: 6px 12px;
}
.zoomclose:hover { color: #fff; }

/* Switch, holding the book while Alfred writes. The one screen that used to
   be an ellipsis. */
/* Switch, holding the book while Alfred writes.
 *
 * John, on a thirty-second wait: "it was long enough that it made me suspect
 * that my prompt wasn't working ... it's a static image, and I don't think
 * it has the same impact as something that's moving."
 *
 * He is right, and a spinner would be the lazy answer -- every site has one
 * and none of them belong to anybody. So Switch moves instead: his tail
 * switches, which is what he is named for, and the whole of him breathes.
 * It says the same thing a spinner says and it says it as this site.
 */
.holding {
  display: flex; align-items: center; gap: 16px;
  margin: 8px 0 26px;
}
.holding img {
  flex: none;
  transform-origin: 50% 85%;        /* his feet, so he sways rather than slides */
  animation: switching 2.4s ease-in-out infinite;
}
@keyframes switching {
  0%   { transform: rotate(-1.6deg) translateY(0); }
  25%  { transform: rotate(1.4deg) translateY(-2px); }
  50%  { transform: rotate(-1.1deg) translateY(0); }
  75%  { transform: rotate(1.8deg) translateY(-1px); }
  100% { transform: rotate(-1.6deg) translateY(0); }
}

/* And the line beside him keeps time, so there is something moving even for
   anyone who never looks at the picture. */
.holding p::after {
  content: "";
  animation: thinkingdots 1.6s steps(4, end) infinite;
}
@keyframes thinkingdots {
  0%   { content: ""; }
  25%  { content: "."; }
  50%  { content: ".."; }
  75%  { content: "..."; }
  100% { content: ""; }
}

/* Somebody who has asked for less movement gets less movement, not none:
   the dots keep time and Switch stands still. */
@media (prefers-reduced-motion: reduce) {
  .holding img { animation: none; }
}
.holding p { color: var(--quiet); font-style: italic; font-size: 19px; }
@media (max-width: 640px) {
  .holding { gap: 12px; }
  .holding img { width: 72px; height: 72px; }
  .holding p { font-size: 17px; }
}

/* --- the pair, on the front door ---------------------------------------
   Snoopy and Woodstock on a warm ground: no words, nothing to read, just
   the two of them. A different arrangement each visit, so the page is never
   quite the same twice. */
/* The front door: one of John's sayings, filling the frame. Alfred and
   Switch used to be arranged here in various ways with nothing to read
   between them; the saying is the reason to come back, so it is now the
   only thing that goes here. */
.scene {
  width: 100%; margin: 0;
  border-radius: 10px;
  overflow: hidden;
}
.scene .whole { display: block; width: 100%; height: auto; }

/* Alfred, while you are talking to him. Small enough to be company rather
   than an illustration of the conversation. */
/* Alfred's first line asks what brings you to his branch of the tree. John,
   reviewing: a reference like that "should be accompanied by a drawing of
   Alfred in the context he is referencing ... That will make the meaning of
   those references visually and immediately clear." */
.listener {
  display: block;
  width: 100%; max-width: 320px; height: auto;
  margin: 0 0 20px;
}
@media (max-width: 640px) { .listener { max-width: 250px; margin-bottom: 14px; } }


/* A card whose picture has not arrived yet. The same gray block the ghosts
   use, so the row does not jump when the real thing lands on top of it. */
.object.waiting .shot { background: rgba(120, 104, 78, 0.1); }
.object.waiting img { opacity: 0; }

/* --- a page of prose --------------------------------------------------- */
/* Privacy, and anything else that is simply read. */
.prose { max-width: var(--read); }
.prose h1 { margin-bottom: var(--s3); }
.prose h2 {
  font-size: 19px; letter-spacing: 0.04em; text-transform: uppercase;
  color: var(--faint); font-weight: normal;
  margin: var(--s5) 0 var(--s2);
}
.prose p { margin-bottom: var(--s2); }
.prose a { color: var(--ink); }
/* ...except a link dressed as a button, which keeps the button's own color.
   `.prose a` is both more specific than `.primary` and later in this file, so
   the button on the confirmation page came out as dark text on a dark
   background. Willy: "there is a black button with dark text so you can't
   read what the button says." That page is the only place a button sits
   inside a block of prose, which is why nothing else showed it. */
.prose a.primary { color: var(--paper); }

.sitefoot a { color: inherit; }

/* Deleting a journal. Quiet until it is asked, and alarming only once it
   has been -- which is the one moment it should be. */
.burn { margin-top: var(--s4); }
.burn .textlink { margin-top: 0; }
.textlink.armed { color: #9a3b32; text-decoration-thickness: 2px; }

/* --- the shop ---------------------------------------------------------- */
/* Choosing a line, before anything is for sale. Nothing here looks like a
   product grid: it is a page of sayings, and picking one is the only thing
   it asks. */
.pick { margin-bottom: var(--s5); }
.picktitle {
  font-size: 17px; font-weight: normal; letter-spacing: 0.06em;
  text-transform: uppercase; color: var(--faint);
  margin-bottom: var(--s3);
}
.lines { list-style: none; margin-bottom: var(--s3); }
.lines li { margin-bottom: var(--s2); }
.line {
  display: block; width: 100%; max-width: var(--read);
  text-align: left;
  background: none; border: 0; border-left: 2px solid transparent;
  padding: var(--s1) var(--s2);
  font: 21px/1.45 var(--serif); color: var(--ink);
  cursor: pointer;
}
.line:hover {
  border-left-color: var(--quiet);
  background: rgba(255, 255, 255, 0.55);
}

/* --- the journal ------------------------------------------------------ */
/* Their own page. The one rule the site keeps holds here too: John's
   sayings are in the hand, and everything else -- the dates, their words,
   anything Alfred or we say -- is in the type. */

/* The only place on the site that needs the hand, so it is the only place
   that pays for it. A font is fetched when something uses it, so declaring
   it here costs the front page nothing. */
@font-face {
  font-family: "Caveat";
  src: url("/fonts/caveat-600.woff2") format("woff2");
  font-weight: 600;
  font-style: normal;
  font-display: swap;
}

a.primary { text-decoration: none; }

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

.journalstate { max-width: var(--read); }
.journalstate p + p { margin-top: var(--s2); }

/* One day per block, newest first, each one the width of the page. */
.days { list-style: none; }
.day {
  padding: var(--s4) 0;
  border-top: 1px solid var(--rule);
}
.day:first-child { border-top: 0; padding-top: 0; }

.dayname {
  font-size: 17px; font-weight: normal; letter-spacing: 0.06em;
  text-transform: uppercase; color: var(--faint);
  margin-bottom: var(--s3);
}

/* What they said, and later what they wrote. Type, italic, held to a
   reading measure -- these are paragraphs, not display. */
/* Each conversation of a day, with a little air between them so two reads
   as two rather than as one long one. */
.visit + .visit { margin-top: var(--s4); }

.dsaid, .dwritten { max-width: var(--read); }
.dsaid { margin-bottom: var(--s3); }
/* Their own writing follows the sayings, so it needs the room the sayings
   do not give it: the hand ends flush against this label otherwise, and the
   two read as one block. */
.dwritten { margin-top: var(--s4); }

/* Saved, and read as part of the page rather than parked in a field. A day
   holds a sequence of these: a diary is written more than once a day. */
.notes { list-style: none; }
.note { margin-bottom: var(--s3); max-width: var(--read); }
.note > p { font-style: italic; color: var(--quiet); white-space: pre-wrap; }
.notehead {
  display: flex; align-items: baseline; gap: var(--s2);
  margin-bottom: 2px;
}
.notetime { font-size: 14px; color: var(--faint); }
.notehead .textlink { margin-top: 0; }

/* "Edit", "Write something of your own". A link, not a button: these are
   small invitations beside somebody's own words, and a slab of dark ink
   would be louder than the writing it offers to change. */
.textlink {
  background: none; border: 0; padding: 0;
  font: 15px/1.6 var(--serif); color: var(--faint);
  text-decoration: underline; cursor: pointer;
  margin-top: var(--s1);
}
.textlink:hover { color: var(--ink); }
/* The way out to the shop. A link rather than a button, for the same reason
   as the rest of these -- but an anchor is inline, so the margin every other
   .textlink gets does nothing until it is told to take one. */
/* The way out of the page, and the only one there is from here. One
   saying is kept at a time, so this is one button and not a list. */
.onsomething { margin-top: var(--s3); }
.textlink.invite { font-style: italic; }

/* The box they write in. It is paper, not a form field: no raised border,
   no gray box, nothing that says "fill this in". */
.writing {
  display: block; width: 100%; max-width: var(--read);
  font: italic 19px/1.65 var(--serif);
  color: var(--ink);
  background: rgba(255, 255, 255, 0.5);
  border: 1px solid var(--rule); border-radius: 3px;
  padding: var(--s2) var(--s3);
  resize: none;                 /* it grows with what is in it instead */
  overflow: hidden;
}
.writing:focus {
  outline: none;
  border-color: var(--quiet);
  background: rgba(255, 255, 255, 0.85);
}
.writing::placeholder { color: var(--faint); font-style: italic; }

/* The promise on the left, what the save is doing on the right. Sharing a
   line means the state needs no reserved space of its own, so nothing jumps
   and there is no blank line when it has nothing to say. */
.writefoot {
  display: flex; justify-content: space-between; align-items: baseline;
  gap: var(--s3);
  max-width: var(--read);
  margin-top: var(--s1);
}
/* .fineprint carries a top margin for its usual job further up the site,
   which here pushed the promise a line below the save state beside it. */
.writefoot .fineprint { margin-top: 0; }
.saved { font-size: 15px; color: var(--faint); white-space: nowrap; }
@media (max-width: 640px) {
  .writefoot { flex-wrap: wrap; gap: var(--s1); }
}
.dsaid p, .dwritten p { font-style: italic; color: var(--quiet); }
.label {
  display: block;
  font-size: 14px; letter-spacing: 0.08em; text-transform: uppercase;
  color: var(--faint); margin-bottom: var(--s1);
}

/* The sayings they kept. The hand, and the only thing on the page in it. */
.kept { list-style: none; }
.kept li {
  font-family: Caveat, "Bradley Hand", cursive;
  font-weight: 600;
  font-size: 30px; line-height: 1.45;
  color: #2f3a54;
  max-width: 24em;
  margin-bottom: var(--s2);
}
.kept li:last-child { margin-bottom: 0; }

/* "Earlier days", under the journal. */
.secondary.wide { margin-top: var(--s4); }

/* Somewhere to go when they reach the bottom. */
.onward {
  margin-top: var(--s5);
  padding-top: var(--s4);
  border-top: 1px solid var(--rule);
  max-width: var(--read);
}
.onward .fineprint { margin-top: var(--s2); }

@media (max-width: 640px) {
  .day { padding: var(--s3) 0; }
  .kept li { font-size: 26px; }
}

/* --- the list ---
   The one block on the front door that exists to be filled in rather than
   read. A rule above it and the reading measure, so it comes after the
   button as the next quiet thing rather than as a banner. */
.join {
  margin-top: var(--s5);
  padding-top: var(--s4);
  border-top: 1px solid var(--rule);
  max-width: var(--read);
}
.join h2 { font-size: 25px; font-weight: normal; margin-bottom: var(--s2); }
.join p { color: var(--quiet); font-size: 17px; }
.join form {
  display: flex; gap: var(--s2); flex-wrap: wrap; align-items: center;
  margin-top: var(--s3);
}
/* Grows with the room and still fits a phone: 240px is narrower than the
   narrowest screen we care about, so the button drops below rather than the
   field shrinking to something nobody can read their own address in. */
.join .field { flex: 1 1 240px; }
.join .primary { margin-top: 0; white-space: nowrap; }
.join .fineprint { color: var(--faint); font-size: 15px; }
/* Empty until there is something to say, and taking up no room while it is:
   an aria-live region has to be in the document before the text arrives, or
   a screen reader never announces it. */
.join .joinsaid { color: var(--ink); margin-top: var(--s3); }
.join .joinsaid:empty { display: none; }
.join .joinsaid.trouble { color: #8c2f24; }

/* --- the cart ---
   A count beside Shop, on every page, so somebody reading their journal can
   see they left something in it. Hidden at zero: "(0)" reads as broken. */
/* The cart, in the navigation.
   It was a bare oval with a number in it, which says nothing. Willy: "it is
   not a cart icon or does not have any visual indication for the user what
   it is." So it is a word, like every other thing in this bar -- the site has
   no icons anywhere and one would arrive as a stranger -- with the count
   beside it when there is something to count. */
.cartlink { display: inline-flex; align-items: baseline; gap: 7px; }
.cartcount {
  font-variant-numeric: tabular-nums;
  background: var(--ink); color: var(--paper);
  border-radius: 999px; padding: 1px 8px 2px;
  font-size: 13px;
}

/* Choosing the details, inside the larger look at a product. One row, one
   baseline, and the same controls as the rest of the site. */
.buying {
  display: flex; align-items: flex-end; gap: var(--s3);
  flex-wrap: wrap;
  padding: var(--s3);
  border-top: 1px solid var(--rule); margin-top: var(--s3);
}
.sizepick { display: block; }
.sizepick span {
  display: block; font-size: 13px; color: var(--faint);
  letter-spacing: 0.08em; text-transform: uppercase; margin-bottom: 5px;
}
/* Our own chevron, because a native one sits hard against the right edge and
   ignores padding -- Willy: "the dropdown arrow on the size selector is
   butted against the right side of the button with no margin." Drawing it
   ourselves also makes the control look the same on every browser instead of
   three different shapes. */
.sizepick select, .cartline select {
  font: 17px/1.4 var(--serif); color: var(--ink);
  background-color: rgba(255, 255, 255, 0.6);
  border: 1px solid var(--rule);
  border-radius: 3px; padding: 12px 38px 12px 13px; min-width: 5em;
  appearance: none; -webkit-appearance: none;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='11' height='7' viewBox='0 0 11 7'%3E%3Cpath d='M1 1l4.5 4.5L10 1' fill='none' stroke='%236b6152' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: right 14px center;
}
.sizepick select:focus, .cartline select:focus {
  outline: 2px solid var(--quiet); outline-offset: -1px;
}
.zoomprice { font-size: 23px; margin-left: auto; line-height: 2.2; }
.buying .primary { margin-top: 0; flex: 0 0 auto; }

@media (max-width: 480px) {
  /* The price above, the size and the button sharing a line below it, both
     full height. Four things on one row at 390px is where this looked
     thrown together. */
  .zoomprice { margin-left: 0; flex: 1 1 100%; line-height: 1.2; }
  .buying .primary { flex: 1 1 auto; }
}

/* After something is added: the two things a person wants next. */
.added {
  display: flex; align-items: center; gap: var(--s3);
  flex-wrap: wrap;
  padding: var(--s3);
  border-top: 1px solid var(--rule);
}
.added p { flex: 1 1 100%; color: var(--quiet); margin-bottom: var(--s1); }
.added .primary { margin-top: 0; }

/* The cart page. */
.cartlines { margin: var(--s4) 0; }
.cartline {
  display: grid;
  grid-template-columns: 90px minmax(0, 1fr) auto auto;
  gap: var(--s3);
  align-items: start;
  padding: var(--s3) 0;
  border-top: 1px solid var(--rule);
}
.cartline img {
  width: 90px; height: auto; border-radius: 4px;
  background: rgba(120, 104, 78, 0.1);
}
.cartline .saying { font-size: 17px; }
.cartline .thing { color: var(--quiet); font-size: 15px; margin-top: 4px; }
.cartline .how { display: flex; flex-direction: column; gap: var(--s1); }
.cartline .linetotal { font-size: 18px; white-space: nowrap; }

@media (max-width: 640px) {
  /* The picture and the words, then the controls underneath. Four columns on
     a phone puts the price in a two-character gutter. */
  .cartline { grid-template-columns: 70px minmax(0, 1fr); }
  .cartline img { width: 70px; }
  .cartline .how { grid-column: 1 / -1; flex-direction: row; align-items: center; }
  .cartline .linetotal { grid-column: 2; text-align: right; }
}

.carttotals { max-width: var(--read); margin-top: var(--s4); }
.carttotals p {
  display: flex; justify-content: space-between; gap: var(--s3);
  color: var(--quiet); padding: 5px 0;
}
.carttotals .beforetax {
  color: var(--ink); font-size: 19px;
  border-top: 1px solid var(--rule); margin-top: var(--s1); padding-top: var(--s2);
}
.carttotals .fineprint { display: block; margin-top: var(--s2); }
.cartactions {
  display: flex; align-items: center; gap: var(--s4);
  flex-wrap: wrap; margin-top: var(--s4);
}
.cartactions .primary { margin-top: 0; }

/* --- a saying on its own page ------------------------------------------
   Somebody arriving here came from a search and has never met Alfred, so
   the order is: the line, who wrote it, one way in, then more lines. Not a
   pitch -- the only thing being asked for is a click, and only after the
   thing they came for. */
.line .square {
  max-width: 420px; margin: 0 0 var(--s4);
  background: #efe9dc; border-radius: 12px; overflow: hidden;
}
.line .square img { display: block; width: 100%; height: auto; }
.line h1 {
  font-size: 34px; line-height: 1.22; font-weight: normal;
  max-width: 16em; margin-bottom: var(--s3);
}
.line .lede { margin-bottom: var(--s4); }
.line .fineprint { max-width: var(--read); }
.alongside { margin-top: var(--s5); border-top: 1px solid var(--rule);
             padding-top: var(--s3); }
.alongside h2, .line section h2 {
  font-size: 14px; letter-spacing: 0.1em; text-transform: uppercase;
  color: var(--faint); font-weight: normal; margin: var(--s4) 0 var(--s2);
}
.lines { list-style: none; max-width: var(--read); }
.lines li { padding: 7px 0; border-bottom: 1px solid rgba(120, 104, 78, 0.13); }
.lines a { color: var(--ink); text-decoration: none; }
.lines a:hover { text-decoration: underline; }
@media (max-width: 560px) {
  .line h1 { font-size: 27px; }
  .line .square { max-width: 100%; }
}

/* --- a page somebody was sent -----------------------------------------
   They did not ask for this and have never met Alfred, so the picture is
   the page and everything else is quiet underneath it. */
.gift .given { max-width: 540px; margin: var(--s4) 0; }
.gift .given img {
  display: block; width: 100%; height: auto;
  border-radius: 4px; box-shadow: 0 2px 14px rgba(80, 66, 42, 0.16);
}
.gift figcaption {
  margin-top: var(--s1); font-size: 15px; color: var(--faint);
}
.gift h2 {
  font-size: 14px; letter-spacing: 0.1em; text-transform: uppercase;
  color: var(--faint); font-weight: normal; margin: var(--s5) 0 var(--s2);
}
.gift p { max-width: var(--read); }

/* --- giving a page away ------------------------------------------------ */
.sendit { margin-top: var(--s4); padding-top: var(--s3);
          border-top: 1px solid var(--rule); max-width: var(--read); }
.sendit .give { color: var(--quiet); font-size: 17px; margin-bottom: var(--s2); }
.sendit form { display: flex; flex-wrap: wrap; gap: var(--s2); align-items: flex-end; }
.sendit label { display: block; font-size: 15px; color: var(--quiet);
                margin-bottom: 5px; }
.sendit input {
  font: 17px/1.5 var(--serif); color: var(--ink); padding: 9px 12px;
  background: rgba(255, 255, 255, 0.55); border: 1px solid var(--rule);
  border-radius: 2px; min-width: 180px;
}
.sentout { margin-top: var(--s3); display: flex; flex-wrap: wrap;
           gap: var(--s2); align-items: center; }
.sentout .yours { flex-basis: 100%; }
.sentout input { flex: 1; min-width: 240px; font-size: 15px; }


/* --- a saying, worn ---------------------------------------------------
   The photograph is the page: a shirt on a person says more than artwork
   on paper. Everything it can go on sits underneath as thumbnails, which
   is how a listing is read everywhere else people buy clothes. */
.line .worn {
  display: flex; gap: var(--s2); max-width: 680px; margin: 0 0 var(--s4);
}
.line .worn img {
  display: block; width: 100%; height: auto; flex: 1; min-width: 0;
  background: #fff; border-radius: 6px;
}
@media (max-width: 560px) {
  .line .worn { flex-direction: column; max-width: 360px; }
}
.onthings { margin-top: var(--s5); }
.tiles {
  list-style: none; display: grid; gap: var(--s2);
  grid-template-columns: repeat(auto-fill, minmax(118px, 1fr));
  max-width: 640px;
}
.tiles a {
  display: block; text-decoration: none; color: var(--ink);
  border: 1px solid var(--rule); border-radius: 2px; overflow: hidden;
  background: #fff;
}
.tiles a:hover { border-color: var(--quiet); }
.tiles img { display: block; width: 100%; height: auto; }
.tiles .what { display: block; font-size: 13px; padding: 6px 8px 0; }
.tiles .cost { display: block; font-size: 13px; color: var(--faint);
               padding: 0 8px 8px; }

/* The photograph at the head of a line's products in the shop. The grid
   beneath it is flat lays, which is right for thumbnails and is not what
   sells a shirt. */
/* One woman and one man, side by side: we do not know who is reading.
   They stack on a phone rather than becoming two thumbnails. */
/* The pair spans the same width as the grid of products below it, so the
   two line up at both edges and the page reads as one column rather than
   a narrow thing above a wide one.

   Willy, 10 October 2026: "It is difficult to read the text on the model
   shots ... Can the model images be larger so that they align left and
   right with the underling column width for the thumb? That would make
   the images larger and also create a box alignment on the page."

   It was capped at 720 and centred, which on a 1080 column left it about
   two thirds the width -- and the saying is a small part of a standing
   figure, so every pixel lost came off the words. */
.wornhere {
  display: grid; grid-template-columns: 1fr 1fr;
  gap: var(--s2);
  margin: 0 0 var(--s3);
}
.wornhere img {
  display: block; width: 100%; height: auto; min-width: 0;
  border-radius: 6px; background: #fff;
}
/* Side by side is what makes the pair read as one garment in two fits.
   Stacked, they are two products. Only below the width where two would
   be too small to see the saying at all. */
@media (max-width: 560px) {
  .wornhere { grid-template-columns: 1fr; }
}

/* --- the shop, dressed for Christmas ---------------------------------- */
/* Willy, 9 October 2026: "we should make the shop page for any christmas
   sayings a holiday page. dress up the page backgound with holiday colors
   and ornamentation. Make the page itself look like holiday cheer."

   In Alfred's own palette rather than pillar-box red and green. His scarf
   is already a Christmas teal and his beret a Christmas burgundy, so the
   page can be dressed without becoming a different brand for a month.

   The class arrives on the body from the server when the URL names a
   Christmas saying, and is put on by shop.js when one is picked from the
   list, so there is no plain page repainting itself a moment later. */
body.holiday {
  /* Warmer and greener than paper, and still quiet enough to read on. */
  background-color: #F4F1E6;
  background-image:
    radial-gradient(circle at 12% 8%, rgba(76, 141, 131, 0.10), transparent 42%),
    radial-gradient(circle at 88% 4%, rgba(168, 78, 78, 0.10), transparent 40%),
    radial-gradient(circle at 50% 100%, rgba(143, 55, 65, 0.07), transparent 55%);
  background-attachment: fixed;
}

/* The garland hangs under the navigation, across the full width. Its own
   element rather than a border image, so it keeps its proportions and can
   be hidden without touching the page it decorates. */
body.holiday .sitenav { border-bottom: 0; }
body.holiday .sitenav::after {
  content: "";
  display: block;
  /* Hung from the bottom of the navigation, not a fixed distance from its
     top. On a phone the links wrap to two rows and a fixed offset drew the
     garland straight through them. */
  position: absolute; left: 0; right: 0; top: 100%;
  height: 56px;
  background: url("/garland.svg") repeat-x center top;
  background-size: auto 56px;
  pointer-events: none;
}
.sitenav { position: relative; }
body.holiday main { padding-top: 46px; }

/* The saying itself, and the two words that introduce the products. */
body.holiday .shopline { color: #7C2F3A; }
body.holiday .picktitle,
body.holiday .journalhead h1 { color: #2F4F49; }

/* Product tiles sit on cards; a touch of warmth keeps them from reading as
   a plain catalogue on a dressed page. */
body.holiday .objects .card,
body.holiday .objects figure { border-color: #D9C9C2; }

@media (prefers-reduced-motion: no-preference) {
  body.holiday .sitenav::after { transition: opacity 240ms ease; }
}
