/* The shop front. GENERATED -- by jtsapps_site/kit/make_shopfront.py from
   kit/shopfront.css.tmpl; edit the template, not this file, and re-run it.

   WHAT THIS IS FOR. Somebody meets JTS on the hub, sees a coloured card,
   clicks it, and lands here. Before this, that landing felt like walking
   out of a shop and into a boiler room: the hub is loud, light and
   typographic, and every product site was the quiet dark working screen
   that a customer stares at doing their figures on a Tuesday night. Two
   different companies, to look at.

   WHAT CHANGED AND WHAT DID NOT. The COLOR SCHEME stays -- this product's
   own ground, dark or light, and its own accent -- because that is the
   product's identity and it is what the working screens are. What is
   shared is everything ELSE: the same two typefaces, the same big display
   scale, the same 2px rules and hard un-blurred offset shadow, the same
   pill buttons, the same fluid width, the same scroll reveals, the same
   flat color-field art panel -- and, since the bug list of 2026-10-04
   (#5, #11, #110), the same header, footer and forms on every site, to the
   markup in specs/buglist-2026-10-04/STANDARDS.md. One company, to look at.

   WHERE IT APPLIES, AND WHY THAT MATTERS. This file is loaded ONLY on the
   public pages -- the sales page, signing up, paying, terms. EVERY rule is
   under `body.shopfront`, and no template inside the app sets that class.
   EZ Move is the reason both of those are belt and braces: its app screens
   and its sales page share one layout and one stylesheet, so an unscoped
   rule here would restyle a tool somebody is working in.

   It is a SEPARATE FILE rather than an addition to style.css for the same
   reason: style.css is untouched, so nothing here can reach the app even
   by accident, and the whole change comes off by deleting one link.

   NAMED PARTS a page can use (the rest of this file draws them):
     header   header.top > a.brand (svg.jts-mark + span.brand-name)
                         + nav.sf-nav (a.sf-tools, a.say-btn.sf-cta, .cog)
              .sf-hide-sm         hidden below 640px: a second button, or
                                  the extra words of a long label
     forms    plain <label for> + box, or the site's .fld / .fld-label
              .sf-hint            the quiet line under a box
              .sf-err             one box's own error, right under it
              .sf-errors          every error, listed at the top of a form
              .sf-ok              the line that says it worked
     footer   .foot (footer, div or p) holding the five links
     grids    .sf-cards           cards that never leave one on its own */

body.shopfront {
  /* Everything is prefixed --sf- on purpose. The app's own variables live
     in the same document on EZ Move, and a shop front must not be able to
     reach in and repaint a working screen. */
  --sf-accent: #9b2335;
  --sf-on-accent: #ffffff;  /* words ON the accent: dark on a bright
                                    one, white on a deep one */
  --sf-art:    #9B2335;          /* this tool's color ON THE HUB, so the
                                    card that was clicked and the panel
                                    that is landed on are the same color */
  --sf-bg:     var(--bg, #0f1216);
  --sf-panel:  var(--surface, #171b21);
  --sf-ink:    var(--ink, #e9eef5);
  --sf-dim:    var(--dim, var(--muted, #94a1b2));
  --sf-edge:   #333c48;
  /* A RULE THAT DIVIDES THE PAGE (the foot's top line) must be seen on a
     phone in daylight: the panel edge above is 1.5:1 on a dark ground and
     all but vanished there (bug list 2026-10-04, #110). The dim text color
     with a third of the ground mixed in: 2.6-3.9:1 on every site's ground,
     dark or light, and still quieter than any words. */
  --sf-rule: color-mix(in srgb, var(--sf-dim) 65%, var(--sf-bg));
  /* THE EDGE OF A BOX YOU TYPE INTO has to be seen to be found (WCAG
     1.4.11 asks 3:1). The panel edge above is a quiet 1.5:1 on a dark
     ground -- right for a card, too faint for a field -- so a field's edge
     is the dim text color pulled a fifth of the way toward the ground:
     at least 3.5:1 on every site's ground and panel, dark or light. */
  --sf-field-edge: color-mix(in srgb, var(--sf-dim) 80%, var(--sf-bg));
  --sf-bad:    var(--bad, var(--red, #e8615f));
  --sf-good:   var(--good, var(--green, #3ecf7a));
  --sf-shadow: 6px 6px 0 rgba(0, 0, 0, .62);
  --sf-wrap:   1720px;
  --sf-gutter: clamp(16px, 2.6vw, 52px);
  --sf-r:      16px;

  padding-left: var(--sf-gutter);
  padding-right: var(--sf-gutter);
  background: var(--sf-bg);
  color: var(--sf-ink);
  font-family: Archivo, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
  font-size: 16.5px;
  line-height: 1.6;
  overflow-x: hidden;
}

body.shopfront h1,
body.shopfront h2,
body.shopfront h3,
body.shopfront .sf-display {
  font-family: "Bricolage Grotesque", Archivo, "Segoe UI", Helvetica, Arial,
               sans-serif;
  font-weight: 800;
  letter-spacing: -.02em;
}

body.shopfront main,
body.shopfront > header.top,
body.shopfront > section,
body.shopfront > div.notice,
body.shopfront > div.strip,
body.shopfront > p.foot,
body.shopfront > footer {
  max-width: var(--sf-wrap);
  margin-left: auto;
  margin-right: auto;
}

/* ---- scroll reveal ------------------------------------------------------
   THE RULE THAT MATTERS MORE THAN THE EFFECT: the hiding only happens once
   the script has said it can un-hide. If the script never runs, the page is
   an ordinary readable page rather than a blank one. A sales page that
   hides its own content and then fails to show it has sold nothing. */
html.can-reveal body.shopfront .reveal {
  opacity: 0;
  transform: translateY(22px);
  transition: opacity .55s cubic-bezier(.22,.7,.3,1),
              transform .55s cubic-bezier(.22,.7,.3,1);
  will-change: opacity, transform;
}
html.can-reveal body.shopfront .reveal.in { opacity: 1; transform: none; }

@media (prefers-reduced-motion: reduce) {
  html.can-reveal body.shopfront .reveal {
    opacity: 1; transform: none; transition: none;
  }
}

/* ---- header -------------------------------------------------------------
   ONE HEADER ON EVERY SITE (bug list #5, STANDARDS section 3):

     <header class="top">
       <a class="brand" href="/">{mark}<span class="brand-name">Shop Rate</span></a>
       <nav class="sf-nav">
         <a class="sf-tools" href="https://jtsapps.com" aria-label="All JTS tools"
            title="Every tool JTS makes">{hexagon}<span class="sf-tools-word">All JTS tools</span></a>
         <a class="say-btn sf-cta" href="/sign-in">Sign in</a>
         <a class="say-btn sf-cta sf-hide-sm" href="#start">Start 14 days free</a>
       </nav>
     </header>

   On a computer: one row -- mark and name, then the way back to the shop,
   then the buttons. On a phone it is STILL one row, and short (56px at
   375px; the target is 64 or less): the name never wraps ("Shop / Rate"
   was the bug), "All JTS tools" is its hexagon alone -- the link's
   aria-label still says the words -- and anything marked .sf-hide-sm steps
   aside so one button is left. Signed in, the button is the way into the
   tool and the .cog is the account; a long label keeps its extra words in
   a <span class="sf-hide-sm"> so a phone reads "Open".

   WHY 640px AND NOT 480. Measured in Edge with every tool's name: the full
   header (name, the words "All JTS tools", two buttons) needs up to ~610px.
   Below that it either compacts or wraps, and a wrapped header was the
   bug, so it compacts from 640px down -- which also covers 480. And from
   860px down "All JTS tools" is already its hexagon and any extra nav
   links (.sf-hide-sm that are not buttons) step aside, so a site that adds
   links still gets one row at 640-860px (see that block below).

   IF A SITE PUTS MORE IN ITS NAV than this, the nav wraps INSIDE ITSELF
   (it may shrink; the name may not). A taller header is a flaw to fix in
   that site's markup; a page that scrolls sideways is broken. */
body.shopfront header.top {
  /* Not stuck to the top: EZ Move's app bar is sticky, and on its public
     pages the see-through shop-front header slid over the words beneath
     it as the page scrolled. Every shop-front header scrolls away. */
  position: static;
  padding: 18px 0 10px;
  display: flex;
  flex-wrap: nowrap;
  align-items: center;
  gap: 14px;
  border: 0;
  background: none;
}
body.shopfront header.top .brand {
  display: flex;
  align-items: center;
  gap: 10px;
  flex: none;                 /* the name is never squeezed... */
  white-space: nowrap;        /* ...and never wraps */
  text-decoration: none;
  color: var(--sf-ink);
  font-family: "Bricolage Grotesque", Archivo, sans-serif;
  font-weight: 800;
  letter-spacing: -.02em;
  font-size: 20px;
  line-height: 1.2;
}
body.shopfront header.top .brand svg { flex: none; }
body.shopfront header.top .jts-mark { width: 34px; height: 34px; }

body.shopfront .sf-nav {
  margin-left: auto;
  display: flex;
  align-items: center;
  gap: 8px;
  flex-wrap: wrap;
  justify-content: flex-end;
  min-width: 0;               /* may shrink and wrap inside itself... */
}
body.shopfront .sf-nav a {
  text-decoration: none;
  color: var(--sf-ink);
  font-size: 15px;
  font-weight: 600;
  line-height: 1.3;
  padding: 9px 14px;
  margin: 0;                  /* (style.css gives .say-btn margin-left:auto) */
  border-radius: 999px;
  border: 2px solid transparent;
  white-space: nowrap;        /* ...but a button's words stay on one line */
}
body.shopfront .sf-nav a:hover { background: var(--sf-panel); }

/* The way back out to the shop. It carries the hexagon so it reads as the
   company mark rather than as one more link. */
body.shopfront .sf-tools {
  position: relative;
  display: inline-flex;
  align-items: center;
  gap: 8px;
  border-color: var(--sf-edge) !important;
}
body.shopfront .sf-tools:hover {
  border-color: var(--sf-accent) !important;
  background: var(--sf-panel);
}
body.shopfront .sf-tools svg { width: 17px; height: 17px; flex: none; }
body.shopfront .sf-nav .sf-cta {
  background: var(--sf-accent);
  color: var(--sf-on-accent);
  border-color: var(--sf-accent);
}
/* (It used to take the plain links' hover -- the panel color -- under its
   own dark words: a button that went dark-on-dark under the pointer.) */
body.shopfront .sf-nav .sf-cta:hover {
  background: var(--sf-accent);
  color: var(--sf-on-accent);
  filter: brightness(1.08);
}
/* TWO BUTTONS SIDE BY SIDE (Sign in, then Start 14 days free): two equal
   filled pills say nothing about which one matters, so where both show, the
   first is drawn as an outline and the second -- the new customer's -- is
   the filled one. On a phone the second steps aside (.sf-hide-sm) and the
   one left is filled. (Without :has() both are filled: still right.) */
@media (min-width: 640px) {
  body.shopfront .sf-nav .sf-cta:has(+ .sf-cta) {
    background: transparent;
    color: var(--sf-ink);
  }
  body.shopfront .sf-nav .sf-cta:has(+ .sf-cta):hover {
    background: var(--sf-panel);
    color: var(--sf-ink);
    filter: none;
  }
}
/* Signed in: the account, as an icon. */
body.shopfront .sf-nav .cog { font-size: 18px; line-height: 1.1; }

/* Chalkline's header used to put its Sign in link OUTSIDE the nav. Until
   every page has it inside (STANDARDS section 3), the order is set here
   rather than by markup: mark, then the nav, then that page's own link. */
body.shopfront header.top .brand { order: -2; }
body.shopfront header.top .sf-nav { order: -1; }
body.shopfront header.top .top-link {
  order: 0;
  flex: none;
  margin: 0;
  text-decoration: none;
  font-size: 15px;
  font-weight: 600;
  line-height: 1.3;
  padding: 9px 14px;
  border-radius: 999px;
  white-space: nowrap;
  box-shadow: none;
}
body.shopfront header.top .top-link:not(.sf-cta) {
  color: var(--sf-ink);
  border: 2px solid var(--sf-accent);
  background: none;
}
body.shopfront header.top .top-link:not(.sf-cta):hover {
  background: var(--sf-accent); color: var(--sf-on-accent); transform: none;
}

/* EZ MOVE'S NAV HAS NO CLASS OF ITS OWN, and it is the same element its app
   screens use, so it cannot be given one without reaching into the app.
   It is matched by position instead -- and only under body.shopfront, so a
   working screen is never touched. */
body.shopfront header.top nav {
  margin-left: auto;
  display: flex;
  align-items: center;
  gap: 8px;
  flex-wrap: wrap;
  justify-content: flex-end;
  min-width: 0;
}
body.shopfront header.top nav a {
  text-decoration: none;
  color: var(--sf-ink);
  font-size: 15px;
  font-weight: 600;
  line-height: 1.3;
  padding: 9px 14px;
  border-radius: 999px;
  border: 2px solid transparent;
  white-space: nowrap;
  background: none;
  box-shadow: none;
}
body.shopfront header.top nav a:hover {
  background: var(--sf-panel); transform: none; box-shadow: none;
}
body.shopfront header.top nav a.on { color: var(--sf-accent); }
body.shopfront header.top nav a.sf-cta {
  background: var(--sf-accent);
  color: var(--sf-on-accent);
  border-color: var(--sf-accent);
}
body.shopfront header.top nav a.sf-cta:hover {
  background: var(--sf-accent); filter: brightness(1.08);
}

/* ---- the header from 640 to 860px (a small tablet, a narrow window) -----
   A SITE MAY CARRY MORE than the standard markup in its nav: Sitework's
   Examples and Pricing, Box 12's Free calculator, each marked .sf-hide-sm.
   Measured in Edge, those plus the words "All JTS tools" and two buttons
   need up to ~800px, so between 640 and 860px the header broke onto two
   rows (Sitework at 660-737px, Box 12 at 640px -- the fix-up to #5). In
   this band the header keeps the same one row a phone gets, only roomier:
   "All JTS tools" is its hexagon, and the extra LINKS step aside (not the
   buttons, and not the account cog). From 860px everything shows. */
@media (max-width: 859.98px) {
  /* "All JTS tools" becomes its hexagon. font-size 0 hides the words even
     on a page whose markup has them bare (no .sf-tools-word yet); the word
     itself is kept for screen readers, which read the link's aria-label. */
  body.shopfront .sf-nav .sf-tools,
  body.shopfront header.top nav .sf-tools {
    padding: 8px;
    gap: 0;
    font-size: 0;
    line-height: 0;
  }
  body.shopfront .sf-tools svg { width: 20px; height: 20px; }
  body.shopfront .sf-tools .sf-tools-word {
    position: absolute;
    width: 1px;
    height: 1px;
    margin: -1px;
    padding: 0;
    border: 0;
    overflow: hidden;
    clip: rect(0 0 0 0);
    clip-path: inset(50%);
    white-space: nowrap;
  }
  /* the extra links (a link marked .sf-hide-sm that is not a button and
     not the cog) */
  body.shopfront .sf-nav a.sf-hide-sm:not(.sf-cta):not(.cog),
  body.shopfront header.top nav a.sf-hide-sm:not(.sf-cta):not(.cog) {
    display: none !important;
  }
}

/* ---- the header on a phone (and anything under 640px) ------------------ */
@media (max-width: 639.98px) {
  body.shopfront header.top { padding: 10px 0 8px; gap: 10px; }
  body.shopfront header.top .brand { font-size: 18px; gap: 8px; }
  body.shopfront header.top .jts-mark { width: 30px; height: 30px; }
  body.shopfront .sf-nav,
  body.shopfront header.top nav { gap: 6px; }
  body.shopfront .sf-nav a,
  body.shopfront header.top nav a,
  body.shopfront header.top .top-link {
    padding: 7px 12px;
    font-size: 14.5px;
    line-height: 20px;
  }
  body.shopfront .sf-nav .cog { padding: 7px 9px; font-size: 18px; }

  /* (the hexagon-only "All JTS tools" is set in the block above, which
     covers phones too; here it only gets the phone's smaller padding) */
  body.shopfront .sf-nav .sf-tools,
  body.shopfront header.top nav .sf-tools { padding: 7px; }

  /* ONE button on a phone: anything marked as the extra steps aside. */
  body.shopfront .sf-hide-sm { display: none !important; }
}
/* The narrowest phones still in use (320px): a little less air, same row
   -- measured with the longest name signed in ("Box 12 Helper", Open and
   the cog). */
@media (max-width: 359.98px) {
  body.shopfront header.top { gap: 8px; }
  body.shopfront header.top .brand { font-size: 16px; gap: 6px; }
  body.shopfront header.top .jts-mark { width: 26px; height: 26px; }
  body.shopfront .sf-nav,
  body.shopfront header.top nav { gap: 4px; }
  body.shopfront .sf-nav a,
  body.shopfront header.top nav a,
  body.shopfront header.top .top-link { padding: 7px 9px; font-size: 14px; }
  body.shopfront .sf-nav .sf-tools,
  body.shopfront header.top nav .sf-tools { padding: 7px; font-size: 0; }
  body.shopfront .sf-nav .cog { padding: 7px 6px; }
}

/* ---- type -------------------------------------------------------------- */
body.shopfront h1 {
  font-size: clamp(34px, 5.6vw, 64px);
  line-height: 1;
  margin: 0 0 18px;
  text-wrap: balance;
}
body.shopfront h2 { font-size: clamp(24px, 3vw, 34px); line-height: 1.08; }
body.shopfront h3 { font-size: 20px; letter-spacing: -.01em; }

body.shopfront .lead,
body.shopfront .sub {
  font-size: clamp(16.5px, 1.7vw, 19px);
  color: var(--sf-dim);
  max-width: 58ch;          /* prose does NOT grow with the page: the extra
                               width goes to the grids instead */
}
body.shopfront .sf-eyebrow {
  font-size: 12.5px;
  font-weight: 700;
  letter-spacing: .13em;
  text-transform: uppercase;
  color: var(--sf-dim);
  margin: 0 0 14px;
}

/* ---- the hero ---------------------------------------------------------- */
body.shopfront .sf-hero {
  display: grid;
  grid-template-columns: 1.25fr 1fr;
  gap: 30px;
  align-items: center;
  padding: 30px 0 8px;
}
@media (max-width: 900px) {
  body.shopfront .sf-hero { grid-template-columns: 1fr; gap: 22px; }
}
body.shopfront .sf-hero-words { min-width: 0; }
body.shopfront .sf-buttons {
  display: flex; flex-wrap: wrap; gap: 12px; margin: 22px 0 16px;
}
body.shopfront .sf-note {
  font-size: 15px;
  color: var(--sf-dim);
  margin: 0;
  padding-left: 14px;
  border-left: 4px solid var(--sf-accent);
}
/* WHAT IT DOES, in two lines: what you put in and what you get back (JT,
   2026-09-30: "make exactly what they do presented on their front page").
   The words come from the hub's tools.py through family.py, so they are the
   same two lines as the card on jtsapps.com. Label beside the words on a
   computer, above them on a phone, where a label column would leave the
   words a sliver. */
body.shopfront .sf-io {
  display: grid;
  gap: 10px;
  margin: 22px 0 0;
  max-width: 62ch;
}
body.shopfront .sf-io > div {
  display: grid;
  grid-template-columns: 7.4em minmax(0, 1fr);
  gap: 4px 16px;
  align-items: baseline;
  padding: 12px 16px;
  background: var(--sf-panel);
  border: 1.5px solid var(--sf-edge);
  border-radius: 12px;
}
body.shopfront .sf-io dt {
  font-size: 12px;
  font-weight: 700;
  letter-spacing: .12em;
  text-transform: uppercase;
  color: var(--sf-accent);
}
body.shopfront .sf-io dd {
  margin: 0;
  font-size: 16.5px;
  line-height: 1.45;
  color: var(--sf-ink);
}
@media (max-width: 560px) {
  body.shopfront .sf-io > div { grid-template-columns: minmax(0, 1fr); }
}

/* THE PICTURE. There are no photographs here and no invented screenshots.
   The image is the product's own promise set large on its own color --
   the same panel the hub puts on its cards, so the click and the landing
   are visibly the same object. It says something true and cannot go
   stale. */
body.shopfront .sf-art {
  position: relative;
  background: var(--sf-art);
  color: #fff;
  border: 2px solid var(--sf-edge);
  border-radius: var(--sf-r);
  box-shadow: var(--sf-shadow);
  padding: 22px;
  min-height: 250px;
  display: flex;
  flex-direction: column;
  overflow: hidden;
}
body.shopfront .sf-art::before {          /* a drawing board, faintly */
  content: "";
  position: absolute; inset: 0;
  background-image: linear-gradient(rgba(255,255,255,.14) 1px, transparent 1px),
                    linear-gradient(90deg, rgba(255,255,255,.14) 1px, transparent 1px);
  background-size: 26px 26px;
  opacity: .5;
}
body.shopfront .sf-art-who {
  position: relative;
  font-size: 11.5px; font-weight: 700; letter-spacing: .13em;
  text-transform: uppercase; opacity: .85;
}
/* THE PANEL IS A SIGN, NOT AN ECHO. It first carried the product's own
   tagline -- which is also the headline three inches to its left, so the
   page said the same sentence twice and the picture earned nothing. It now
   carries what a sign carries: the trade, the name, and the one concrete
   thing underneath. */
body.shopfront .sf-art-name {
  position: relative;
  margin-top: auto;
  font-family: "Bricolage Grotesque", Archivo, sans-serif;
  font-weight: 800;
  letter-spacing: -.03em;
  font-size: clamp(38px, 4.4vw, 58px);
  line-height: .95;
  text-wrap: balance;
}
body.shopfront .sf-art-say {
  position: relative;
  margin-top: 10px;
  font-size: 15px;
  line-height: 1.35;
  opacity: .92;
  font-variant-numeric: tabular-nums;
}
body.shopfront .sf-art-mark {
  position: absolute;
  right: -10px; bottom: -40px;
  font-family: "Bricolage Grotesque", Archivo, sans-serif;
  font-weight: 800;
  font-size: 170px;
  line-height: 1;
  color: rgba(255, 255, 255, .13);
  pointer-events: none;
}

/* ---- panels ------------------------------------------------------------
   The existing pages are built out of .card, and they keep it -- the
   construction is what changes: a real 2px rule and a hard un-blurred
   offset, the way the hub builds a panel, instead of a soft raised box. */
body.shopfront .card,
body.shopfront .sf-panel {
  background: var(--sf-panel);
  border: 2px solid var(--sf-edge);
  border-radius: var(--sf-r);
  box-shadow: var(--sf-shadow);
  padding: 24px;
  margin: 22px 0;
}
body.shopfront .card > h2:first-child,
body.shopfront .sf-panel > h2:first-child { margin-top: 0; }

body.shopfront .sf-cards {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(min(100%, 315px), 1fr));
  gap: 20px;
  margin: 22px 0;
}
body.shopfront .sf-cards .card,
body.shopfront .sf-cards .sf-panel { margin: 0; }
/* NO CARD LEFT ON ITS OWN (bug list #34, #37). Filling the row with as
   many 315px cards as fit put three cards 2 + 1 on a 737px screen, and
   four cards 3 + 1 on a laptop -- a hole beside the last one. Three cards
   are three across or one above another; four are four, two by two, or
   one. (A browser without :has() keeps the plain rule above.) */
body.shopfront .sf-cards:has(> :nth-child(3):last-child) {
  grid-template-columns: minmax(0, 1fr);
}
body.shopfront .sf-cards:has(> :nth-child(4):last-child) {
  grid-template-columns: minmax(0, 1fr);
}
@media (min-width: 600px) {
  body.shopfront .sf-cards:has(> :nth-child(4):last-child) {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }
}
@media (min-width: 900px) {
  body.shopfront .sf-cards:has(> :nth-child(3):last-child) {
    grid-template-columns: repeat(3, minmax(0, 1fr));
  }
}
@media (min-width: 1200px) {
  body.shopfront .sf-cards:has(> :nth-child(4):last-child) {
    grid-template-columns: repeat(4, minmax(0, 1fr));
  }
}

body.shopfront .sf-head { margin: 66px 0 22px; max-width: 60ch; }
body.shopfront .sf-head h2 { margin: 0 0 8px; }
body.shopfront .sf-head p { margin: 0; color: var(--sf-dim); }

/* ---- buttons -----------------------------------------------------------
   Same gesture as the hub: a pill with a real border and a hard offset
   that presses in when clicked. There the fill is gold on paper; here it
   is this product's accent on its own ground. */
body.shopfront button,
body.shopfront .go,
body.shopfront .btn,
body.shopfront .say-btn,
body.shopfront input[type="submit"] {
  font-family: "Bricolage Grotesque", Archivo, sans-serif;
  font-weight: 800;
  letter-spacing: -.01em;
  font-size: 16px;
  padding: 14px 24px;
  border-radius: 999px;
  border: 2px solid var(--sf-accent);
  background: var(--sf-accent);
  color: var(--sf-on-accent);
  text-decoration: none;
  display: inline-block;
  cursor: pointer;
  box-shadow: var(--sf-shadow);
  transition: transform .12s, box-shadow .12s, filter .12s;
}
body.shopfront button:hover,
body.shopfront .go:hover,
body.shopfront .btn:hover,
body.shopfront .say-btn:hover {
  transform: translate(-2px, -2px);
  box-shadow: 8px 8px 0 rgba(0, 0, 0, .62);
  filter: brightness(1.06);
}
body.shopfront button:active,
body.shopfront .go:active,
body.shopfront .btn:active,
body.shopfront .say-btn:active {
  transform: translate(3px, 3px);
  box-shadow: 2px 2px 0 rgba(0, 0, 0, .62);
}
body.shopfront .btn-quiet,
body.shopfront button.quiet {
  background: transparent;
  color: var(--sf-ink);
  border-color: var(--sf-edge);
  box-shadow: none;
}
body.shopfront .btn-quiet:hover,
body.shopfront button.quiet:hover {
  background: var(--sf-panel);
  box-shadow: none;
  transform: none;
}
/* The header's own buttons are nav, not calls to action -- they must not
   grow a hard shadow and start shouting at the top of the page. */
body.shopfront header.top .say-btn,
body.shopfront .sf-nav a {
  box-shadow: none;
  font-family: Archivo, sans-serif;
  font-weight: 600;
  letter-spacing: 0;
}
body.shopfront header.top .say-btn {
  padding: 9px 14px;
  font-size: 15px;
  line-height: 1.3;
  margin: 0;
}
body.shopfront header.top .say-btn:hover,
body.shopfront .sf-nav a:hover { transform: none; box-shadow: none; }
@media (max-width: 639.98px) {
  body.shopfront header.top .say-btn {
    padding: 7px 12px;
    font-size: 14.5px;
    line-height: 20px;
  }
}
@media (max-width: 359.98px) {
  body.shopfront header.top .say-btn { padding: 7px 10px; font-size: 14px; }
}

body.shopfront a.linkish,
body.shopfront .linkish {
  color: var(--sf-accent);
  background: none;
  border: 0;
  box-shadow: none;
  padding: 0;
  font: inherit;
  text-decoration: underline;
  text-underline-offset: 3px;
}
body.shopfront a.linkish:hover { transform: none; box-shadow: none; }

/* ---- the robot trap (a honeypot) ---------------------------------------
   A form may carry one box only a robot fills in. People must never see it
   (Lineup's contact page showed "Leave this empty" under the message box:
   fix-up to #110). It is moved off the screen rather than display:none,
   which some robots skip; aria-hidden and tabindex="-1" in the markup keep
   it from screen readers and the Tab key. Name it .sf-trap (or the older
   .hp, or data-trap); .lu-trap is Lineup's name for it. Like every rule in
   this file it stays under body.shopfront (each site's test_shopfront.py
   refuses a rule that reaches its app screens); app forms hide their own. */
body.shopfront .sf-trap, body.shopfront .hp, body.shopfront .lu-trap,
body.shopfront [data-trap] {
  position: absolute !important;
  left: -10000px !important;
  top: auto !important;
  width: 1px !important;
  height: 1px !important;
  overflow: hidden !important;
  margin: 0 !important;
  padding: 0 !important;
  border: 0 !important;
}

/* ---- forms -------------------------------------------------------------
   ONE LOOK FOR EVERY FORM ON THE SHOP FRONT (bug list #110; STANDARDS 5,
   7 and 8): signing up, signing in, choosing a new password, the contact
   page. The plain markup works with nothing added --

     <form method="post" action="/contact">
       <label for="c-email">Your email</label>
       <input id="c-email" name="email" type="email" autocomplete="email" required>
       <label for="c-text">Your message</label>
       <textarea id="c-text" name="text" rows="6" required></textarea>
       <button type="submit">Send</button>
     </form>

   -- and so does a site's own wrapper (.fld with .fld-label, or a <label>
   round its box). Under a box: <span class="sf-hint"> for the quiet line
   ("8 characters or more") and <span class="sf-err" id=..> for its own
   error, with aria-invalid="true" and aria-describedby on the box. At the
   top: .sf-errors for every error at once, .sf-ok for "Got it". The
   names a site already used -- .fld-note, .warn.bad, .err, .warn.good,
   and for one box's error .fld-err (nine sites), .field-err (Chalkline)
   and .fld-error (Box 12) -- are drawn the same way. */
body.shopfront input[type="text"],
body.shopfront input[type="email"],
body.shopfront input[type="password"],
body.shopfront input[type="tel"],
body.shopfront input[type="url"],
body.shopfront input[type="search"],
body.shopfront input[type="number"],
body.shopfront input[type="date"],
body.shopfront input:not([type]),
body.shopfront select,
body.shopfront textarea {
  background: var(--sf-bg);
  border: 2px solid var(--sf-edge);
  border-color: var(--sf-field-edge);
  border-radius: 10px;
  color: var(--sf-ink);
  font: 16px/1.4 Archivo, "Segoe UI", sans-serif;   /* 16px: a phone
                                    does not zoom the page to type in it */
  padding: 11px 13px;
  max-width: 100%;            /* a box never pushes a phone page sideways */
  box-sizing: border-box;
}
body.shopfront textarea { line-height: 1.5; resize: vertical; }
body.shopfront input::placeholder,
body.shopfront textarea::placeholder { color: var(--sf-dim); opacity: 1; }
body.shopfront input:focus-visible,
body.shopfront select:focus-visible,
body.shopfront textarea:focus-visible,
body.shopfront button:focus-visible,
body.shopfront a:focus-visible {
  outline: 3px solid var(--sf-accent);
  outline-offset: 2px;
}

/* Every label's words look the same, whatever holds them. */
body.shopfront form > label {
  font-size: 15px;
  font-weight: 600;
  line-height: 1.35;
  color: var(--sf-ink);
}
/* ...a tick box or a choice sits beside its words, which are not shouted. */
body.shopfront form > label:has(> input[type="checkbox"]) {
  display: flex;
  align-items: center;
  gap: 10px;
  font-weight: 500;
}
body.shopfront form > label:has(> input[type="radio"]) {
  display: flex;
  align-items: center;
  gap: 10px;
  font-weight: 500;
}
body.shopfront form > label:has(> input[type="checkbox"]) input,
body.shopfront form > label:has(> input[type="radio"]) input {
  width: 18px;
  height: 18px;
  margin: 0;
  flex: none;
  accent-color: var(--sf-accent);
}
/* THE STANDARDS MARKUP: a label that names its box with `for`, and the box
   straight after it, both right inside the form. Told apart by the `for`
   (a label wrapped round its box needs none), so a form a site lays out
   itself -- a grid, a row, a .fld wrapper -- keeps its own spacing, and
   no browser needs :has() to draw this one. */
body.shopfront form > label[for] {
  display: block;
  margin: 18px 0 6px;
}
body.shopfront form > label[for]:first-of-type { margin-top: 0; }
body.shopfront form > label[for] + input[type="text"],
body.shopfront form > label[for] + input[type="email"],
body.shopfront form > label[for] + input[type="password"],
body.shopfront form > label[for] + input[type="tel"],
body.shopfront form > label[for] + input[type="url"],
body.shopfront form > label[for] + input[type="number"],
body.shopfront form > label[for] + input[type="search"],
body.shopfront form > label[for] + input:not([type]),
body.shopfront form > label[for] + select,
body.shopfront form > label[for] + textarea {
  display: block;
  width: 100%;
  max-width: 36rem;
}
body.shopfront form > label[for] + textarea { min-height: 9.5em; }
body.shopfront form > label[for] ~ button,
body.shopfront form > label[for] ~ input[type="submit"] { margin-top: 20px; }
body.shopfront form > label[for] ~ label:not([for]) { margin-top: 16px; }
/* A site's own field wrapper keeps its layout; its words match. */
body.shopfront .fld-label {
  font-size: 15px;
  font-weight: 600;
  line-height: 1.35;
  color: var(--sf-ink);
  letter-spacing: 0;
  text-transform: none;
}
body.shopfront .sf-hint,
body.shopfront .fld-note {
  display: block;
  margin: 6px 0 0;
  font-size: 14px;
  font-weight: 400;
  line-height: 1.45;
  color: var(--sf-dim);
}
/* An error says what to do, next to the box it is about, in a red that
   reads on this ground (the tool's own red, pulled toward the ink). */
body.shopfront .sf-err,
body.shopfront .fld-err,
body.shopfront .field-err,
body.shopfront .fld-error {
  display: block;
  margin: 6px 0 0;
  padding: 0;
  font-size: 14.5px;
  font-weight: 600;
  line-height: 1.4;
  letter-spacing: 0;
  text-transform: none;
  color: var(--sf-bad);
  color: color-mix(in srgb, var(--sf-bad) 70%, var(--sf-ink));
}
body.shopfront .fld-err::before,
body.shopfront .field-err::before,
body.shopfront .fld-error::before { content: none; }
body.shopfront .fld-err[hidden],
body.shopfront .field-err[hidden],
body.shopfront .fld-error[hidden],
body.shopfront .sf-err[hidden] { display: none; }
body.shopfront [aria-invalid="true"] { border-color: var(--sf-bad); }
body.shopfront .sf-errors,
body.shopfront .warn.bad,
body.shopfront .err {
  display: block;
  border: 2px solid var(--sf-bad);
  border-radius: 12px;
  background: var(--sf-panel);
  background: color-mix(in srgb, var(--sf-bad) 12%, var(--sf-panel));
  color: var(--sf-ink);
  padding: 12px 16px;
  margin: 0 0 18px;
  font-size: 15px;
  line-height: 1.5;
}
body.shopfront .sf-errors ul { margin: 6px 0 0; padding-left: 20px; }
body.shopfront .sf-errors a,
body.shopfront .warn.bad a,
body.shopfront .err a { color: var(--sf-ink); font-weight: 700; }
body.shopfront .sf-ok,
body.shopfront .warn.good {
  display: block;
  border: 2px solid var(--sf-good);
  border-radius: 12px;
  background: var(--sf-panel);
  background: color-mix(in srgb, var(--sf-good) 12%, var(--sf-panel));
  color: var(--sf-ink);
  padding: 12px 16px;
  margin: 0 0 18px;
  font-size: 15px;
  line-height: 1.5;
}

/* ---- the rest of the family -------------------------------------------
   The way back to the shop, at the foot of every public page. It is also
   the honest answer to "what else is there?" without making the page about
   the other products. */
body.shopfront .sf-family { margin: 70px 0 0; }
body.shopfront .sf-family-grid {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: 16px;
}
/* NO TOOL LEFT ON ITS OWN (bug list #11, the same hole as #34). The family
   is eleven cards. Filling rows with as many 250px cards as fit put them
   2 + 2 + 2 + 2 + 2 + 1 on a 560-899px screen: one card alone with a hole
   beside it. The columns are now set per width -- 1, 2, 3, then 4 -- and
   whatever is left over in the last row grows to fill it, so every row is
   full for any number of tools. */
@media (min-width: 560px) and (max-width: 899.98px) {
  body.shopfront .sf-family-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  body.shopfront .sf-family-grid > :last-child:nth-child(2n+1) { grid-column: 1 / -1; }
}
@media (min-width: 900px) and (max-width: 1199.98px) {
  body.shopfront .sf-family-grid { grid-template-columns: repeat(3, minmax(0, 1fr)); }
  body.shopfront .sf-family-grid > :last-child:nth-child(3n+1) { grid-column: 1 / -1; }
  body.shopfront .sf-family-grid > :last-child:nth-child(3n+2) { grid-column: span 2; }
}
@media (min-width: 1200px) {
  body.shopfront .sf-family-grid { grid-template-columns: repeat(4, minmax(0, 1fr)); }
  body.shopfront .sf-family-grid > :last-child:nth-child(4n+1) { grid-column: 1 / -1; }
  body.shopfront .sf-family-grid > :last-child:nth-child(4n+2),
  body.shopfront .sf-family-grid > :nth-last-child(2):nth-child(4n+1),
  body.shopfront .sf-family-grid > :last-child:nth-child(4n+3) { grid-column: span 2; }
}
body.shopfront .sf-sib {
  display: flex;
  flex-direction: column;
  text-decoration: none;
  color: var(--sf-ink);
  border: 2px solid var(--sf-edge);
  border-radius: var(--sf-r);
  overflow: hidden;
  background: var(--sf-panel);
  transition: transform .14s, box-shadow .14s, border-color .14s;
}
body.shopfront .sf-sib:hover {
  transform: translate(-3px, -3px);
  box-shadow: 7px 7px 0 rgba(0, 0, 0, .62);
  border-color: var(--sf-c);
}
body.shopfront .sf-sib-art {
  background: var(--sf-c);
  color: #fff;
  padding: 14px 16px 16px;
  min-height: 104px;
  display: flex;
  flex-direction: column;
  gap: 6px;              /* the who-line and the say-line never touch */
  border-bottom: 2px solid var(--sf-edge);
}
/* Plain white, no fading: white is at least 5.06:1 on every tool colour
   in tools.py; at 85% it fell to 4.16-4.28 on three of them. */
body.shopfront .sf-sib-who {
  font-size: 10.5px; font-weight: 700; letter-spacing: .12em;
  text-transform: uppercase; color: #fff;
}
body.shopfront .sf-sib-say {
  margin-top: auto;
  font-family: "Bricolage Grotesque", Archivo, sans-serif;
  font-weight: 800;
  letter-spacing: -.02em;
  font-size: 15.5px;
  line-height: 1.15;
}
body.shopfront .sf-sib-body {
  padding: 13px 16px 15px;
  display: flex;
  align-items: baseline;
  gap: 10px;
}
body.shopfront .sf-sib-body b {
  font-family: "Bricolage Grotesque", Archivo, sans-serif;
  font-weight: 800;
  font-size: 18px;
  letter-spacing: -.02em;
}
/* "Open ->" IN WORDS THAT READ ON THIS CARD (bug list #11). It was drawn in
   each tool's own color, and the deep ones -- Caliper's slate, Drawline's
   navy -- all but vanished on a dark card (1.5:1). It is now this page's
   own ink with a third of the tool's color mixed in: still that tool's
   hue, and at least 7:1 on every site's card, dark or light. (A browser
   without color-mix() shows plain ink.) */
body.shopfront .sf-sib-go {
  margin-left: auto;
  font-size: 14px;
  font-weight: 700;
  white-space: nowrap;
  color: var(--sf-ink);
  color: color-mix(in srgb, var(--sf-c) 30%, var(--sf-ink));
}
body.shopfront .sf-all {
  display: inline-block;
  margin-top: 18px;
  font-weight: 700;
  color: var(--sf-accent);
  text-decoration: none;
  border-bottom: 2px solid var(--sf-accent);
  padding-bottom: 2px;
}

/* ---- foot --------------------------------------------------------------
   ONE FOOTER (bug list #72, #110; STANDARDS section 4). Each site keeps its
   own element and classes -- footer.foot, div.foot or p.foot -- and carries
   the same five links in the same order,
       Terms · Privacy · Contact · <alias>@jtsapps.com · All JTS tools
   and at most one short line about the tool. Drawn the same everywhere: a
   rule above, quiet words, links that stand out from the dots between
   them by weight, each link kept whole on one line, and rows tall enough
   to hit the right link with a thumb when they wrap on a phone. */
body.shopfront .foot {
  max-width: var(--sf-wrap);
  margin: 64px auto 0;
  padding: 22px 0 54px;
  border-top: 2px solid var(--sf-rule);
  color: var(--sf-dim);
  font-size: 14px;
  line-height: 2.1;
  text-align: left;
}
body.shopfront .foot p { margin: 0; }
body.shopfront .foot p + p,
body.shopfront .foot .tiny {
  margin-top: 4px;
  line-height: 1.5;
  font-size: 13.5px;
}
body.shopfront .foot a {
  color: var(--sf-dim);
  font-weight: 600;
  text-decoration: none;
  white-space: nowrap;
  padding: 5px 0;
  margin: 0;          /* the dots space the links; a site's own side margin
                         pushed the first link in off the rule (EZ Move) */
}
body.shopfront .foot a:hover {
  color: var(--sf-ink);
  text-decoration: underline;
  text-underline-offset: 3px;
}

/* ---- a page's own title ------------------------------------------------ */
/* Contact, terms, privacy, sign in: the title sits the same distance under
   the header on every site. Half the sites had 4px and the h1 all but
   touched the header (Milepost /contact, bug list 2026-10-04, #110); the
   other half 22-26px. The space below stays each site's own. */
body.shopfront .pagehead { margin-top: 26px; }

/* ---- notices ----------------------------------------------------------- */
body.shopfront .notice,
body.shopfront .strip {
  border: 2px solid var(--sf-edge);
  border-radius: 12px;
  background: var(--sf-panel);
  padding: 11px 16px;
  margin: 12px 0;
  font-size: 15px;
}
body.shopfront .notice a,
body.shopfront .strip a { color: var(--sf-accent); font-weight: 700; }

/* Tables keep their own scroller so a wide one never makes the whole page
   slide sideways. */
body.shopfront .tbl { width: 100%; }
body.shopfront .sf-scroll { overflow-x: auto; }
