/* =========================================================================
   Tallystick v2, "Local Engine"

   Redesign direction, ported from the approved standalone mock
   (tallystick-redesign-v2.html) onto this build's real markup:

     light first   cool paper ground, hairline grid (Precision Ledger, 2026-09-17)
     mint -> sky   the single accent system: actions carry the gradient,
                   links and markers carry mint alone
     Inter         UI and display face, self-hosted variable, 400..800
     JetBrains Mono every number, code path and status label

   Elevation still reads in opposite directions per theme: raised surfaces go
   LIGHTER than the page in dark, pure WHITE plus a real shadow in light.
   --bg / --surface / --surface-2 remain roles; components pick roles.

   LIGHT FIRST (owner decision 2026-09-17, tokens.css Precision Ledger): the
   base :root IS the light palette, so a no-JS visitor gets light and a
   device's dark preference flips nothing. Only an explicit saved choice,
   localStorage "tallystick-theme" = "dark" via the header toggle, selects
   dark. THEME_SCRIPT (build.py) already falls back to "light" and picks the
   theme before first paint, so the no-wrong-first-frame guarantee holds.
   ========================================================================= */

@font-face{
  font-family:"Inter"; font-style:normal; font-weight:400 800; font-display:swap;
  src:url(/fonts/inter-latin.woff2) format("woff2");
  unicode-range:U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD;
}
@font-face{
  font-family:"Inter"; font-style:normal; font-weight:400 800; font-display:swap;
  src:url(/fonts/inter-latin-ext.woff2) format("woff2");
  unicode-range:U+0100-02BA,U+02BD-02C5,U+02C7-02CC,U+02CE-02D7,U+02DD-02FF,U+0304,U+0308,U+0329,U+1D00-1DBF,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20C0,U+2113,U+2C60-2C7F,U+A720-A7FF;
}

/* The palette lives in tokens.css, which every page loads before this sheet.
   Nothing in app.css may open a :root block: check_tokens() in .gen/build.py
   fails the build over one, because a second declaration of a token is how the
   two sheets drifted apart in the first place. */

*{box-sizing:border-box}
[hidden]{display:none !important}
html{scroll-behavior:smooth}
@media (prefers-reduced-motion:reduce){ html{scroll-behavior:auto} }

/* ---- site-wide ambient, as in the mock -----------------------------------
   Three blurred aurora orbs and one faint grid, fixed behind everything so
   the page never reads as flat white or flat black. Cards stay translucent
   (see --panel) for this to come through as glass. */
.aura{
  position:fixed; inset:0; z-index:-2; overflow:hidden;
  background:radial-gradient(ellipse 80% 50% at 50% -20%, var(--aura-hi), transparent 60%), var(--bg);
}
.orb{position:absolute; border-radius:50%; filter:blur(110px); opacity:var(--orb-op); will-change:transform}
.orb-1{width:560px;height:560px;left:-140px;top:-180px;background:var(--orb-1);animation:drift1 26s ease-in-out infinite alternate}
.orb-2{width:480px;height:480px;right:-120px;top:60px;background:var(--orb-2);animation:drift2 32s ease-in-out infinite alternate}
.orb-3{width:420px;height:420px;left:38%;top:52%;background:var(--orb-3);animation:drift3 38s ease-in-out infinite alternate}
@keyframes drift1{to{transform:translate(90px,70px)}}
@keyframes drift2{to{transform:translate(-80px,110px)}}
@keyframes drift3{to{transform:translate(-60px,-90px)}}
.gridlines{
  position:fixed; inset:0; z-index:-1; pointer-events:none;
  background-image:
    linear-gradient(var(--gl-line) 1px, transparent 1px),
    linear-gradient(90deg, var(--gl-line) 1px, transparent 1px);
  background-size:72px 72px;
  /* One-off, and not a colour: inside a mask, #000 means fully opaque and
     transparent means cut away. The palette has nothing to say about it. */
  mask-image:radial-gradient(ellipse 90% 62% at 50% 0%, #000 30%, transparent 78%);
  -webkit-mask-image:radial-gradient(ellipse 90% 62% at 50% 0%, #000 30%, transparent 78%);
}
@media (prefers-reduced-motion:reduce){ .orb{animation:none} }
body{
  margin:0; background:var(--bg); color:var(--ink);
  font-family:var(--sans); font-size:15.5px; line-height:1.6;
  -webkit-font-smoothing:antialiased; text-rendering:optimizeLegibility;
  transition:background-color .25s ease, color .25s ease;
}
h1,h2,h3{margin:0; text-wrap:balance; font-family:var(--cond); font-weight:800}
h1{font-size:clamp(34px,5vw,60px); line-height:1.04; letter-spacing:-.035em}
.hero-center h1{font-size:clamp(42px,6.6vw,82px); line-height:1.02; letter-spacing:-.045em}
h2{font-size:clamp(23px,2.7vw,31px); line-height:1.12; letter-spacing:-.028em}
h3{font-size:17px; line-height:1.25; letter-spacing:-.012em; font-weight:700}
p{margin:0}
a{color:var(--accent); text-decoration:none}
a:hover{text-decoration:underline; text-underline-offset:2px}
:focus-visible{outline:2px solid var(--accent); outline-offset:2px; border-radius:4px}
.wrap{max-width:var(--col); margin:0 auto; padding:0 var(--gutter)}
.sr{position:absolute; width:1px; height:1px; overflow:hidden; clip:rect(0 0 0 0); white-space:nowrap}
.skip{
  position:absolute; left:12px; top:-60px; z-index:60;
  background:var(--accent); color:var(--accent-ink); font-weight:600;
  padding:10px 16px; border-radius:var(--r-sm); transition:top .15s;
}
.skip:focus{top:12px; text-decoration:none}

/* ------------------------------------------------------------------ header */
.site-head{
  position:sticky; top:0; z-index:30;
  background:var(--head-glass); backdrop-filter:blur(16px);
  -webkit-backdrop-filter:blur(16px);
  border-bottom:1px solid var(--line);
  transition:background-color .25s ease, border-color .25s ease;
}
.site-head .wrap{display:flex; align-items:center; gap:24px; height:var(--head-h)}
.brand{
  display:inline-flex; align-items:center; gap:10px; color:var(--ink);
  font-family:var(--cond); font-weight:800; font-size:19px; letter-spacing:-.02em;
  flex:0 0 auto;
}
.brand:hover{text-decoration:none}
.brand svg{width:27px; height:27px; display:block; border-radius:7px; flex:0 0 auto;
  box-shadow:0 0 14px rgba(61,232,160,.22)}
.brand b{font-weight:800}
.site-nav{display:flex; gap:22px; margin-left:2px}
.site-nav a{color:var(--muted); font-size:14.5px; font-weight:500; padding:6px 0; position:relative;
  transition:color .15s}
.site-nav a:hover{color:var(--ink); text-decoration:none}
.site-nav a::after{
  content:""; position:absolute; left:0; right:0; bottom:0; height:2px;
  background:var(--grad); border-radius:2px;
  transform:scaleX(0); transform-origin:left;
  transition:transform .2s cubic-bezier(.3,.7,.4,1);
}
.site-nav a:hover::after{transform:scaleX(1)}
.head-right{margin-left:auto; display:flex; align-items:center; gap:18px}
.head-right a{font-size:14.5px; color:var(--muted); transition:color .15s}
.head-right a:hover{color:var(--ink); text-decoration:none}
.head-right a[aria-current="page"]:not(.btn){color:var(--ink); font-weight:600}
/* .head-right a is a class+type selector and outranks .btn-primary, so the
   header CTA was painting its label in --muted on the bright gradient: 1.9:1,
   unreadable in both themes. Restate the button ink, hover included. */
.head-right a.btn-primary,.head-right a.btn-primary:hover{color:var(--accent-ink)}
.icon-btn{
  display:grid; place-items:center; width:36px; height:36px; padding:0; cursor:pointer;
  border:1px solid var(--line-strong); border-radius:10px;
  background:transparent; color:var(--muted);
  transition:background .16s, color .16s, border-color .16s, transform .16s;
  position:relative; overflow:hidden;
}
.icon-btn:hover{background:var(--accent-soft); color:var(--accent); border-color:var(--accent); transform:translateY(-1px)}
.icon-btn:active{transform:scale(.94)}

/* Mobile menu, built on <details> so it needs no JavaScript and keeps the
   keyboard and screen-reader behaviour the browser already gives a disclosure. */
.menu{display:none; position:relative}
.menu > summary{list-style:none; cursor:pointer}
.menu > summary::-webkit-details-marker{display:none}
.menu-panel{
  position:absolute; right:0; top:calc(100% + 10px); width:min(78vw,264px);
  background:var(--surface); border:1px solid var(--line); border-radius:var(--r);
  box-shadow:var(--shadow-3); padding:8px; display:flex; flex-direction:column;
  animation:menuIn .18s cubic-bezier(.3,.9,.4,1) both;
}
@keyframes menuIn{from{opacity:0; transform:translateY(-6px) scale(.98)}to{opacity:1; transform:none}}
.menu-panel > a{
  color:var(--ink-2); font-size:15px; padding:10px 12px; border-radius:var(--r-xs);
  transition:background .13s, color .13s;
}
.menu-panel > a:hover{background:var(--accent-soft); color:var(--ink); text-decoration:none}
@media (max-width:960px){
  .site-nav{display:none}
  .menu{display:block}
  .head-right .hide-sm{display:none}
}

/* ------------------------------------------------------------------- hero --
   Dark instrument panel: aurora wash, faint engineering grid, and the tool as
   the brightest object on screen. In light mode the band stays dark on purpose,
   so crossing pages still reads as one site. */
.hero{
  background:var(--band); color:var(--band-ink); position:relative; overflow:hidden;
  padding:clamp(44px,5.8vw,76px) 0 clamp(38px,4.8vw,60px);
  border-bottom:1px solid var(--line);
}
.hero::before{
  content:""; position:absolute; inset:0; pointer-events:none;
  background:
    radial-gradient(90% 120% at 78% -10%, rgba(65,191,240,.16) 0%, transparent 55%),
    radial-gradient(70% 100% at 12% 110%, rgba(61,232,160,.12) 0%, transparent 55%);
}
.hero::after{
  content:""; position:absolute; inset:0; pointer-events:none;
  background-image:
    linear-gradient(rgba(255,255,255,.03) 1px, transparent 1px),
    linear-gradient(90deg, rgba(255,255,255,.03) 1px, transparent 1px);
  background-size:64px 64px;
  /* One-off, and not a colour: inside a mask, #000 means fully opaque and
     transparent means cut away. The palette has nothing to say about it. */
  mask-image:radial-gradient(ellipse 95% 80% at 50% 0%, #000 40%, transparent 80%);
  -webkit-mask-image:radial-gradient(ellipse 95% 80% at 50% 0%, #000 40%, transparent 80%);
}
:root[data-theme="light"] .hero::after{
  background-image:
    linear-gradient(rgba(255,255,255,.05) 1px, transparent 1px),
    linear-gradient(90deg, rgba(255,255,255,.05) 1px, transparent 1px);
}
.hero-in{
  position:relative; z-index:1; display:grid; gap:clamp(28px,3.6vw,52px);
  grid-template-columns:minmax(0,0.95fr) minmax(0,1.05fr); align-items:center;
}
.hero-copy{display:flex; flex-direction:column; gap:16px; min-width:0}
.hero-copy > *{animation:heroRise .55s cubic-bezier(.2,.7,.3,1) both}
.hero-copy > *:nth-child(2){animation-delay:.06s}
.hero-copy > *:nth-child(3){animation-delay:.12s}
.hero-copy .panel{animation-delay:.16s}
@keyframes heroRise{from{opacity:0; transform:translateY(16px)}to{opacity:1; transform:none}}
.hero h1{color:var(--band-hi)}
/* Set exactly like .page-intro p in page.css, which is the same sentence in the
   same role on the hand-written pages: interface face, 17px, 1.6, pretty
   wrapping. The two were tried apart and read as two different sites. The
   closing sentence carries full ink. */
.hero-sub{
  color:var(--band-muted); font-size:17px; line-height:1.6; text-wrap:pretty;
  max-width:46ch;
}
.hero-center .hero-sub{max-width:56ch}
.hero-sub .stays{color:var(--band-hi); font-weight:600}
/* Nugroho, 2026-08-28: the intro link carried an accent underline; he asked
   for the line gone on every page ("Excel nach XRechnung"). The link stays
   white and still shifts to accent on hover, so it reads as a link without
   the rule. */
.hero-sub a{color:var(--band-hi); text-decoration:none}
.hero-sub a:hover{color:var(--accent); text-decoration:none}
.marks{list-style:none; margin:8px 0 0; padding:0; display:flex; flex-direction:column; gap:10px}
.marks li{position:relative; padding-left:26px; font-size:15.5px; color:var(--band-ink)}
.marks li::before{
  content:""; position:absolute; left:0; top:.14em; width:16px; height:16px;
  background-color:var(--accent);
  -webkit-mask:var(--mark-check) center/contain no-repeat;
          mask:var(--mark-check) center/contain no-repeat;
}

/* the tool panel */
.panel{
  background:var(--v5-surface); color:var(--v5-ink);
  border:1px solid var(--v5-line-strong); border-radius:10px;
  box-shadow:0 1px 2px rgba(23,33,43,.05);
  overflow:hidden; min-width:0;
}
/* no grey band: the mock's panel is one surface, and the tab strip sits on it
   with an amber underline rather than inside a bar of its own */
.panel-head{
  display:flex; align-items:center; gap:10px; padding:8px 24px 0;
  border-bottom:none; background:none;
}
.panel-title{
  font:600 12px/1 var(--sans); letter-spacing:.4px; text-transform:uppercase;
  color:var(--v5-muted);
}
.panel-body{padding:24px}
.dot-live{
  width:8px; height:8px; border-radius:50%; background:var(--ok); flex:0 0 auto;
  box-shadow:0 0 0 3px var(--ok-soft); position:relative;
}
.dot-live::after{
  content:""; position:absolute; inset:-4px; border-radius:50%;
  border:1px solid var(--ok); animation:livePulse 2.2s ease-out infinite;
}
@keyframes livePulse{from{transform:scale(.55); opacity:.9}to{transform:scale(1.55); opacity:0}}
.panel-note{margin-left:auto; font:600 12px/1 var(--sans); letter-spacing:.4px;
  text-transform:uppercase; color:var(--v5-muted)}

.drop{
  position:relative;
  border:1.5px dashed var(--line-strong); border-radius:var(--r);
  background:color-mix(in srgb, var(--bg) 55%, transparent);
  padding:26px 20px; text-align:center; display:flex; flex-direction:column;
  align-items:center; gap:4px;
  transition:border-color .18s, background .18s, box-shadow .18s;
}
.drop.over,.drop.working{
  border-color:var(--accent); border-style:solid;
  background:var(--accent-soft);
  /* No ring and no inner glow. Both were drawn in rgba(61,232,160), the dark
     language's green, on a page whose one accent is amber: a second accent
     nobody chose, appearing only while a file is being read. */
  box-shadow:none;
}
.drop.over .drop-title,.drop.working .drop-title{color:var(--accent)}
/* Loading state, drawn purely in CSS off the class app.js already toggles:
   a scan band sweeps the drop zone while files are being read. */
.drop.working::before{
  content:""; position:absolute; left:8%; right:8%; bottom:14px; height:3px;
  border-radius:3px; overflow:hidden;
  background:linear-gradient(90deg, transparent, var(--accent), transparent);
  background-size:40% 100%; background-repeat:no-repeat;
  animation:scanX 1.1s linear infinite;
}
@keyframes scanX{from{background-position:-40% 0}to{background-position:140% 0}}
.drop-title{font-family:var(--cond); font-size:18px; font-weight:700; letter-spacing:-.015em}
.drop-sub{color:var(--muted); font-size:13.5px; max-width:40ch}
.drop .btn{margin-top:10px}
.drop-formats{
  display:flex; flex-wrap:wrap; justify-content:center; gap:5px; margin-top:16px;
  padding-top:14px; border-top:1px solid var(--line); width:100%;
}
.drop-formats span{
  font-family:var(--mono); font-size:10.5px; font-weight:500; letter-spacing:.05em;
  color:var(--muted);
  border:1px solid var(--line); border-radius:100px; padding:3px 9px; background:transparent;
  transition:border-color .15s, color .15s, transform .15s;
}
.drop-formats span:hover{border-color:var(--accent); color:var(--accent); transform:translateY(-1px)}
.under-drop{
  display:flex; flex-wrap:wrap; gap:6px 8px; align-items:center; justify-content:center;
  /* bottom padding, because .tool-card has none of its own: without it this
     line sits on the card's bottom border. */
  padding:14px 2px 20px; font-size:14px; color:var(--muted);
}
.under-drop .dot{color:var(--line-strong)}
.privacy{display:inline-flex; align-items:center; gap:6px; color:var(--ok); font-weight:500}

@media (max-width:940px){
  .hero-in{grid-template-columns:1fr}
  .hero-copy{gap:14px}
  .marks{flex-direction:row; flex-wrap:wrap; gap:8px 26px}
}
@media (max-width:620px){
  .marks{flex-direction:column; gap:9px}
}

/* proof strip, Nugroho, 2026-08-27: redesigned as the same raised card every
   other block wears: translucent panel, the one gradient rule along the top,
   a live pulse dot beside the counter and the number set in gradient-clipped
   tabular mono. The "0 files uploaded, ever" line was removed at the same
   time; the strip now leads with the only number that moves. */
.proof{
  position:relative; overflow:hidden;
  display:flex; flex-wrap:wrap; align-items:center; gap:10px 30px;
  padding:16px 20px; margin:0 auto; max-width:var(--col);
  border:1px solid var(--line); border-radius:var(--r);
  background:linear-gradient(180deg, var(--panel-hi), var(--panel));
  backdrop-filter:blur(8px); -webkit-backdrop-filter:blur(8px);
  box-shadow:var(--shadow-1);
}
.proof::before{
  content:""; position:absolute; left:0; right:0; top:0; height:2.5px;
  background:var(--grad);
}
.proof-wrap{padding:22px 22px 0}
.proof-item{display:flex; align-items:center; gap:11px; font-size:13.5px; color:var(--ink-2); letter-spacing:.01em}
.proof-item::before{
  content:""; flex:0 0 auto; width:7px; height:7px; border-radius:50%;
  background:var(--accent); box-shadow:0 0 10px rgba(61,232,160,.65);
  animation:proofPulse 2.4s ease-in-out infinite;
}
@keyframes proofPulse{0%,100%{opacity:1}50%{opacity:.4}}
@media (prefers-reduced-motion:reduce){ .proof-item::before{animation:none} }
.proof-item b{
  font-family:var(--mono); font-variant-numeric:tabular-nums;
  font-size:26px; font-weight:600; letter-spacing:-.02em; line-height:1;
  background:var(--grad); -webkit-background-clip:text; background-clip:text; color:transparent;
}
/* the counter bump, applied by fx.js whenever the number changes */
@keyframes countPop{0%{transform:scale(1)}35%{transform:scale(1.18); color:var(--accent)}100%{transform:scale(1)}}
.proof-item b.pop{animation:countPop .45s cubic-bezier(.3,.8,.4,1); display:inline-block}
.proof-note{margin-left:auto; font-size:12.5px; color:var(--muted); max-width:52ch}
@media (max-width:760px){ .proof-note{margin-left:0} }

/* ----------------------------------------------------------------- buttons */
.btn{
  position:relative; overflow:hidden;
  font:inherit; font-size:15px; font-weight:600; cursor:pointer;
  border:1px solid var(--line-strong); background:transparent; color:var(--ink);
  padding:9px 16px; border-radius:10px; display:inline-flex;
  align-items:center; justify-content:center; gap:7px; white-space:nowrap;
  transition:background .16s, border-color .16s, box-shadow .16s, transform .16s, color .16s;
}
.btn:hover{background:var(--accent-soft); border-color:var(--accent); color:var(--accent);
  text-decoration:none; transform:translateY(-1px)}
/* A pseudo-class raises specificity above .btn-primary's plain 0,1,0, so this
   single line used to wipe the gradient off EVERY primary button on hover
   (ghost fill, accent-ink label stuck dark on the header) while its glow
   stayed: the "button disappeared" report. The 0,3,0 compound selector wins
   regardless of file order. */
.btn.btn-primary:hover,
.btn.btn-primary:active{
  background:var(--grad);
}
.btn:active{transform:scale(.96)}
.btn-primary{
  background:var(--grad); border:none; color:var(--accent-ink);
  font-weight:700; box-shadow:0 4px 18px rgba(61,232,160,.30);
}
.btn-primary:hover{box-shadow:0 6px 26px rgba(61,232,160,.45); transform:translateY(-1px); color:var(--accent-ink);
  filter:saturate(1.08)}
.btn-primary::after{
  content:""; position:absolute; top:0; left:-80%; width:45%; height:100%;
  background:linear-gradient(100deg, transparent, rgba(255,255,255,.42), transparent);
  transform:skewX(-20deg); animation:sheen 4.6s ease-in-out infinite;
}
@keyframes sheen{0%,58%{left:-80%}78%,100%{left:135%}}
.btn-lg{font-size:16px; padding:12px 22px; border-radius:12px}
.head-cta{font-size:13px; padding:8px 15px; border-radius:10px; letter-spacing:-.01em}
.btn:disabled{opacity:.45; cursor:not-allowed; transform:none !important}
/* busy state: fx.js adds .busy while an export or submit is running */
.btn.busy{pointer-events:none; color:transparent !important}
/* The label is blanked while busy, so the spinner must NOT depend on
   currentColor: currentColor IS the transparent that just hid the label, which
   is exactly how a clicked button used to go fully invisible (bare arc on a
   ghost button, nothing at all on the gradient). Colours are resolved from
   tokens instead: --ink for buttons sitting on page surfaces, --accent-ink --
   the gradient's own label colour -- for .btn-primary. */
.btn.busy::before{
  content:""; position:absolute; width:16px; height:16px; border-radius:50%;
  border:2px solid color-mix(in srgb, var(--ink) 22%, transparent);
  border-top-color:var(--accent);
  animation:spin .7s linear infinite;
}
.btn-primary.busy::before{
  border-color:color-mix(in srgb, var(--accent-ink) 30%, transparent);
  border-top-color:var(--accent-ink);
}
@keyframes spin{to{transform:rotate(360deg)}}
.btn-link{border:0; background:none; color:var(--accent); padding:0; font-size:14px;
  cursor:pointer; font-family:inherit; position:relative}
.btn-link:hover{text-decoration:underline; background:none}
/* click ripple, appended by fx.js */
.fx-ripple{
  position:absolute; border-radius:50%; pointer-events:none;
  background:currentColor; opacity:.22;
  transform:scale(0); animation:ripple .5s ease-out forwards;
}
@keyframes ripple{to{transform:scale(2.6); opacity:0}}

/* ------------------------------------------------------------------- tool */
.tool{padding:0 0 8px}
.tool-intro{display:flex; flex-direction:column; gap:8px}

/* file tabs */
.files{display:flex; flex-wrap:wrap; gap:8px; margin-top:22px}
.file-tab{
  display:flex; align-items:center; gap:8px; font-size:14px;
  border:1px solid var(--line); background:var(--surface); color:var(--ink-2);
  padding:7px 12px; border-radius:10px; cursor:pointer; font-family:inherit;
  max-width:280px; position:relative; overflow:hidden;
  transition:border-color .15s, background .15s, color .15s, transform .15s;
}
.file-tab .nm{overflow:hidden; text-overflow:ellipsis; white-space:nowrap}
.file-tab:hover{border-color:var(--line-strong); transform:translateY(-1px)}
.file-tab:active{transform:scale(.97)}
.file-tab[aria-selected="true"]{border-color:var(--accent); background:var(--accent-soft); color:var(--ink)}
.file-tab.bad{border-color:var(--bad); color:var(--bad)}
/* The per-file OK pill repeated the verdict the facts card already states,
   and a switcher with one entry is not a switcher: both stay down. */
.file-tab .pill{display:none}
.files:has(.file-tab:only-child){display:none}

/* results */
.results{margin:26px 0 12px; display:none; flex-direction:column; gap:18px; scroll-margin-top:84px}
.results.on{display:flex}
.results.on > *{animation:resultIn .42s cubic-bezier(.2,.7,.3,1) both}
.results.on > *:nth-child(1){animation-delay:.02s}
.results.on > *:nth-child(2){animation-delay:.06s}
.results.on > *:nth-child(3){animation-delay:.10s}
.results.on > *:nth-child(4){animation-delay:.14s}
.results.on > *:nth-child(5){animation-delay:.18s}
.results.on > *:nth-child(6){animation-delay:.22s}
.results.on > *:nth-child(n+7){animation-delay:.26s}
@keyframes resultIn{from{opacity:0; transform:translateY(12px)}to{opacity:1; transform:none}}
.bar{
  display:flex; flex-wrap:wrap; align-items:center; gap:16px;
  padding:12px 16px; border:1px solid var(--line); border-radius:var(--r);
  background:var(--head-glass); backdrop-filter:blur(14px); -webkit-backdrop-filter:blur(14px);
  box-shadow:var(--shadow-1);
  position:sticky; top:calc(var(--head-h) + 8px); z-index:12;
}
.bar .lbl{
  font-family:var(--mono); font-size:11px; font-weight:600; text-transform:uppercase;
  letter-spacing:.12em; color:var(--muted); margin-right:2px;
}
.bar .doc-ico{width:14px; height:16px; flex:0 0 auto}
/* The row wears the Create page's download-button grammar: one ink frame,
   one type weight, sand hover (owner law 2026-09-18). */
.bar .btn{
  min-height:38px; padding:6px 13px; gap:8px;
  background:var(--v5-surface); border:1px solid var(--v5-ink); border-radius:8px;
  font-size:13px; font-weight:600; line-height:1; color:var(--v5-ink);
  transition:background-color .12s, transform .12s;
}
.bar .btn:hover{
  background:var(--v5-sand); border-color:var(--v5-ink); color:var(--v5-ink);
  transform:none; text-decoration:none;
}
.bar .btn:active{transform:scale(.96)}
.bar .btn:focus-visible{outline:2px solid var(--v5-ink); outline-offset:2px}
.pill{
  display:inline-flex; align-items:center; gap:6px; font-family:var(--mono);
  font-size:12px; font-weight:500; letter-spacing:.02em;
  padding:4px 11px; border-radius:100px; border:1px solid transparent;
  transition:transform .15s;
}
.pill.ok{background:var(--ok-soft); color:var(--ok); border-color:var(--ok)}
.pill.warn{background:var(--warn-soft); color:var(--warn); border-color:var(--warn)}
.pill.bad{background:var(--bad-soft); color:var(--bad); border-color:var(--bad)}
.pill.neutral{background:var(--surface-2); color:var(--ink-2); border-color:var(--line-strong)}

.card{
  border:1px solid var(--line); border-radius:var(--r); background:var(--surface);
  box-shadow:var(--shadow-1); overflow:hidden;
  transition:border-color .25s ease, background-color .25s ease;
}
.card > h2, .card > .card-head{
  padding:13px 16px; border-bottom:1px solid var(--line); background:var(--surface-2);
  font-family:var(--mono); font-size:11.5px; font-weight:600; text-transform:uppercase;
  letter-spacing:.1em; color:var(--muted);
  display:flex; align-items:center; gap:10px;
}
.card-body{padding:16px}

.summary{display:grid; grid-template-columns:repeat(auto-fit,minmax(160px,1fr)); gap:16px 22px}
.field{display:flex; flex-direction:column; gap:2px; min-width:0}
.field .k{
  font-family:var(--mono); font-size:10.5px; text-transform:uppercase; letter-spacing:.09em;
  color:var(--muted); font-weight:500;
}
.field .v{font-size:15.5px; overflow-wrap:anywhere}
.field .v.num{font-family:var(--mono); font-variant-numeric:tabular-nums}
.field .v.big{font-family:var(--mono); font-variant-numeric:tabular-nums; font-size:22px; font-weight:600}

.parties{display:grid; grid-template-columns:1fr 1fr; gap:0}
.parties > div{padding:16px}
.parties > div + div{border-left:1px solid var(--line)}
.party-name{font-weight:600; margin-bottom:2px}
.party-line{color:var(--ink-2); font-size:14.5px}
.party-id{font-family:var(--mono); font-variant-numeric:tabular-nums; font-size:13px; color:var(--muted); margin-top:6px}
@media (max-width:640px){
  .parties{grid-template-columns:1fr}
  .parties > div + div{border-left:0; border-top:1px solid var(--line)}
}

.table-scroll{
  overflow-x:auto; border:1px solid var(--line); border-radius:var(--r);
  background:var(--surface);
}
.card .table-scroll{border:0; border-radius:0}
table{border-collapse:collapse; width:100%; font-size:14.5px; color:var(--ink); background:transparent}
th,td{
  padding:11px 14px; text-align:left; border-bottom:1px solid var(--line); vertical-align:top;
  color:var(--ink);
}
thead th{
  font-family:var(--mono); font-size:10.5px; text-transform:uppercase; letter-spacing:.09em;
  color:var(--muted); font-weight:500; background:var(--surface-2); white-space:nowrap;
  position:sticky; top:0;
}
tbody tr:last-child td{border-bottom:0}
td.num,th.num{text-align:right; font-family:var(--mono); font-variant-numeric:tabular-nums; white-space:nowrap}
td.idx{color:var(--muted); font-family:var(--mono); font-variant-numeric:tabular-nums; width:1%}
section.block tbody td:first-child{font-weight:600; color:var(--ink); white-space:nowrap}
/* The honest rule table on /en16931-validator (#rule-index) on a phone. Its
   uppercase mono headers read as one unbreakable word each (nowrap above),
   which held the table at 407px inside a 344px container at 390: the last
   column sat 40px past the edge of a scroll nothing on screen advertised,
   while page-level scroll stayed 0. Below 560 the headers wrap instead - the
   way the pricing comparison table already fits 390, by wrapping its cells,
   not by scrolling - and the cells give back four pixels a side. Measured
   2026-09-27 with a failing file dropped: table 407px -> 344px, every column
   on screen, page scroll still 0. */
@media (max-width:560px){
  #rule-index thead th{white-space:normal}
  /* side padding 10 -> 8: the pl twin's one-word header floor
     ("sprawdzonych") leaves the table 6px over a 390 container even with
     every break allowed; 2px a side buys it back with slack (pl 334px,
     en/de/fr/it all 344 or less, measured 2026-09-27). */
  #rule-index th, #rule-index td{padding:9px 8px}
  /* the twins describe the families in de/fr/it/pl, and a German compound
     ("Split-Payment-Kategorie:") holds a wider floor than any 390px column
     can give: measured, the de table stayed 389px wide with every header
     free to wrap. Letting the description break inside a word drops its
     floor to the widest fragment - de measures 344px, every column on
     screen, the same as EN. */
  #rule-index tbody td:nth-child(2){overflow-wrap:anywhere}
}
tbody tr{transition:background .12s}
tbody tr:hover td{background:var(--row-hover)}
tfoot td{font-weight:600; border-top:1px solid var(--line-strong); background:var(--surface-2); color:var(--ink)}

.totals{display:grid; grid-template-columns:1fr auto; gap:7px 26px; max-width:430px; margin-left:auto}
.totals .k{color:var(--ink-2); font-size:14.5px}
.totals .v{font-family:var(--mono); font-variant-numeric:tabular-nums; text-align:right; font-size:14.5px}
.totals .grand{
  font-family:var(--mono); font-weight:600; font-size:21px; padding-top:8px;
  border-top:2px solid var(--line-strong);
}
.totals .grand.k{font-size:15px; color:var(--ink); font-family:var(--sans); font-weight:600}

.note{font-size:14px; color:var(--muted)}
.err-link{margin-left:6px; color:inherit; text-decoration:underline; font-weight:600}
.err{
  border:1px solid var(--bad); border-left:4px solid var(--bad);
  background:var(--bad-soft); color:var(--bad);
  padding:13px 16px; border-radius:var(--r-sm); font-size:14.5px;
  animation:errIn .3s cubic-bezier(.2,.8,.3,1) both;
}
@keyframes errIn{from{opacity:0; transform:translateY(-6px)}to{opacity:1; transform:none}}

/* ---------------------------------------------------- EN 16931 findings ----*/
.rules{display:flex; flex-direction:column; gap:2px}
.rule-sign{margin-top:14px; padding-top:12px; border-top:1px solid var(--line); grid-column:1 / -1}
.rule-sign .btn{margin:0 0 8px}
.rule-sign .bad{color:var(--bad)}
.rule{
  display:grid; grid-template-columns:minmax(96px,110px) 1fr; gap:14px;
  padding:11px 12px; border-radius:var(--r-sm); align-items:start;
  border-left:3px solid transparent;
}
.rule-error{background:var(--bad-soft); border-left-color:var(--bad)}
.rule-warning{background:var(--warn-soft); border-left-color:var(--warn)}
.rule-tag{display:flex; flex-direction:column; gap:2px; min-width:0}
.rule-id{
  font-family:var(--mono); font-weight:600; font-size:12.5px; letter-spacing:.03em;
  color:var(--ink);
}
.rule-bt{
  font-family:var(--mono); font-size:10px; letter-spacing:.07em;
  text-transform:uppercase; color:var(--muted);
}
.rule-text{font-size:14.5px; line-height:1.5; color:var(--ink-2); overflow-wrap:anywhere}
.rule-none{font-size:14.5px; color:var(--muted); max-width:var(--measure)}
@media (max-width:560px){
  .rule{grid-template-columns:1fr; gap:4px}
  .rule-tag{flex-direction:row; gap:10px; align-items:baseline}
}

/* ------------------------------------------------------------- feedback ---*/
/* The form is one glass card in a centered column: the same 780px measure,
   panel gradient and blur as the FAQ cards a section above, so the foot of the
   page reads as one system instead of a form left over from another site. */
.fb{
  display:flex; flex-direction:column; gap:12px;
  max-width:780px; margin:36px auto 0;
  border:1px solid var(--line); border-radius:18px;
  background:linear-gradient(180deg, var(--panel-hi), var(--panel));
  backdrop-filter:blur(8px); -webkit-backdrop-filter:blur(8px);
  padding:clamp(22px,3vw,30px);
}
.fb-label{
  font-family:var(--mono); font-size:10.5px; text-transform:uppercase; letter-spacing:.11em;
  color:var(--muted); font-weight:500;
}
.fb-text,.fb-input{
  font:inherit; font-size:15px; color:var(--ink); background:var(--bg);
  border:1px solid var(--line-strong); border-radius:10px; padding:11px 13px; width:100%;
  transition:border-color .16s, box-shadow .16s;
}
.fb-text{resize:vertical; min-height:104px; line-height:1.55}
.fb-text::placeholder,.fb-input::placeholder{color:var(--muted)}
.fb-text:focus,.fb-input:focus{border-color:var(--accent); outline:none;
  box-shadow:0 0 0 3px var(--accent-soft)}
.fb-row{display:flex; gap:12px; align-items:flex-end; flex-wrap:wrap}
.fb-field{display:flex; flex-direction:column; gap:6px; flex:1 1 280px}
.fb-note{font-size:13px; color:var(--muted); max-width:62ch}
.fb-done{
  border:1px solid var(--ok); border-left:4px solid var(--ok);
  background:var(--ok-soft); color:var(--ok);
  padding:13px 16px; border-radius:var(--r-sm); font-size:14.5px; font-weight:500;
  animation:resultIn .35s ease both;
}

/* -------------------------------------------------------- static content --*/
section.block{padding:clamp(40px,4vw,60px) 0; scroll-margin-top:var(--head-h)}
section.block + section.block{border-top:1px solid var(--line)}
section.block h2 + *{margin-top:24px}
section.block h2 + p.lead{margin-top:8px}
/* .sec-sub declares 12px and never got it: the generic h2 + * rule above
   carries one more type selector and wins, so every section intro sat a
   full 24px below its heading. */
section.block h2 + p.sec-sub{margin-top:12px}
/* The lede is the page.css section-intro component, carried to this skin for
   the validator's rule table so both skins define it identically. 12px is the
   family h2 step (the same exception sec-sub needed: the generic h2 + * rule
   above carries fewer selectors and would win), 18px the lede's own floor
   before whatever follows it. */
.lede{color:var(--ink-2); font-size:16.5px; max-width:var(--measure); margin:0 0 18px}
section.block h2 + p.lede{margin-top:12px}
/* --ink-2, not --muted. This rule is (0,2,3) and beat the v5 `.lead` (0,1,0),
   so the mock's colour was written and never landed: the main prose on every
   tool page rendered at #8B887C on #FDFBF4, which is 3.43:1 and fails WCAG AA
   for body text. --ink-2 is 9.08:1 on the same ground. The muted token is for
   labels and glosses, never for the paragraph a visitor came to read. */
section.block > .wrap > p.lead{
  color:var(--ink-2); margin-bottom:32px; max-width:var(--measure); font-size:16.5px;
  text-wrap:pretty;
}
section.block:nth-of-type(even){background:color-mix(in srgb, var(--surface) 45%, var(--bg))}
section.block:nth-of-type(even) .card,
section.block:nth-of-type(even) .table-scroll,
section.block:nth-of-type(even) .fmt{background:var(--surface)}

.grid-3{display:grid; grid-template-columns:repeat(auto-fit,minmax(250px,1fr)); gap:22px}
.step{
  display:flex; flex-direction:column; gap:6px; padding:20px 22px;
  border:1px solid var(--line); border-radius:var(--r);
  background:linear-gradient(180deg, var(--panel-hi), var(--panel));
  backdrop-filter:blur(8px); -webkit-backdrop-filter:blur(8px);
  box-shadow:var(--shadow-1);
  transition:transform .18s, border-color .18s, box-shadow .18s;
}
.step:hover{transform:translateY(-3px); border-color:var(--accent); box-shadow:var(--shadow-2)}
.step .n{
  display:flex; gap:3px; align-items:center; margin-bottom:6px;
  font-family:var(--mono); font-size:11px; font-weight:600; color:var(--muted);
  letter-spacing:.12em; text-transform:uppercase;
}
.step .n i{width:3px; height:14px; background:var(--accent); border-radius:1px; display:block}
.step .n i:last-of-type{margin-right:7px}
.step p{color:var(--ink-2); font-size:15px}

.fmt-groups{display:grid; grid-template-columns:repeat(auto-fit,minmax(300px,1fr)); gap:26px}
.fmt-group h3{
  font-size:12px; text-transform:uppercase; letter-spacing:.12em; color:var(--muted);
  margin-bottom:12px; display:flex; align-items:center; gap:9px; font-family:var(--mono);
  font-weight:600;
}
.fmt-group h3::after{content:""; flex:1 1 auto; height:1px; background:var(--line)}
.fmt-list{display:flex; flex-wrap:wrap; gap:8px}
.fmt{
  font-family:var(--mono); font-size:12px; font-weight:500; letter-spacing:.01em;
  padding:5px 12px; border-radius:100px;
  border:1px solid var(--line-strong); color:var(--ink-2); background:var(--bg);
  transition:border-color .15s, color .15s, transform .15s;
}
.fmt:hover{transform:translateY(-1px); border-color:var(--accent); color:var(--accent)}
.fmt-w{border-color:var(--accent); color:var(--accent); background:var(--accent-soft); font-weight:600}

/* faq */
.faq{display:flex; flex-direction:column; gap:0; border-top:1px solid var(--line)}
.faq details{border-bottom:1px solid var(--line)}
.faq summary{
  padding:16px 0; cursor:pointer; font-weight:600; font-size:16.5px;
  list-style:none; display:flex; align-items:center; gap:12px; color:var(--ink);
  transition:color .15s;
}
.faq summary::-webkit-details-marker{display:none}
/* Plus, closing to a minus, identical to page.css and to .qa-q on /help.
   The rotating chevron was the dark language's glyph. */
.faq summary::before{
  content:"+"; flex:0 0 auto; width:14px; text-align:center;
  color:var(--v5-amber-deep); font:400 20px/1 var(--sans);
}
.faq details[open] summary::before{content:"\2212"}
/* ink text, amber underline: --v5-amber-deep as TEXT is 3.57:1 on paper
   and 3.63:1 on surface, both under 4.5, and a hover state is text too.
   Swept together so the rule is the same everywhere. */
.faq summary:hover{color:var(--v5-ink); text-decoration:underline; text-decoration-color:var(--v5-amber); text-underline-offset:3px}
.faq details p{padding:0 0 18px 26px; color:var(--ink-2); font-size:15.5px; max-width:var(--measure);
  animation:faqIn .28s ease both}
@keyframes faqIn{from{opacity:0; transform:translateY(-4px)}to{opacity:1; transform:none}}

.out-list{
  list-style:none; margin:0; padding:0; display:grid; gap:10px;
  grid-template-columns:repeat(auto-fit,minmax(310px,1fr)); max-width:var(--col);
}
.out-list li{position:relative; padding-left:24px; font-size:15.5px; color:var(--ink-2)}
.out-list li::before{
  content:""; position:absolute; left:0; top:.12em; width:15px; height:15px;
  background-color:var(--accent);
  -webkit-mask:var(--mark-check) center/contain no-repeat;
          mask:var(--mark-check) center/contain no-repeat;
}
section.block#related .note{margin-top:16px}

/* The related-tools block. Three short columns rather than the one flat
   paragraph it used to be: a visitor who came to convert a single format can
   see, without reading a sentence, that the neighbouring conversions are one
   click away. The rule the copy follows is that an anchor says what the
   visitor gets, never what the page is called. */
.rel-grid{
  display:grid; grid-template-columns:repeat(auto-fit,minmax(260px,1fr));
  gap:26px 32px; margin-top:6px;
}
.rel-col h3{
  font-size:12px; text-transform:uppercase; letter-spacing:.12em; color:var(--muted);
  margin-bottom:12px; display:flex; align-items:center; gap:9px; font-family:var(--mono);
  font-weight:600;
}
.rel-col h3::after{content:""; flex:1 1 auto; height:1px; background:var(--line)}
.rel-list{list-style:none; margin:0; padding:0; display:flex; flex-direction:column; gap:11px}
.rel-list li{font-size:15px; line-height:1.5; color:var(--muted)}
.rel-list a{
  color:var(--ink); font-weight:600; text-decoration:none;
  border-bottom:1px solid var(--line-strong);
  transition:color .15s, border-color .15s;
}
.rel-list a:hover{color:var(--accent); border-color:var(--accent)}
/* The note that says when you want this link. It used to trail the anchor
   after an em dash, which the house style bans and which read as one run-on
   sentence. On its own line it is what it always was: a caption about the
   link above it. The anchor carries a border-bottom, so the gap has to clear
   that rule rather than sit on it. */
/* --ink-2, not --muted. Measured against this block's ground: in light mode
   --muted (#64748B on #F2F5F9) is 4.35:1, under the 4.5 that text this size
   needs, while --ink-2 is 8.5:1 and dark mode goes 6.76 to about 11. The
   anchor above still dominates on weight and its underline, so the note reads
   as secondary without being the palest thing on the page. The sibling
   .rel-col h3 is still on --muted and still measures 4.35 in light mode. */
.rel-note{
  display:block; margin-top:4px;
  font-size:13.5px; line-height:1.45; color:var(--ink-2);
}
@media (max-width:560px){ .rel-grid{grid-template-columns:1fr; gap:22px} }

/* --------------------------------------------------------------- tool grid */
.tool-grid{display:grid; grid-template-columns:repeat(3,1fr); gap:16px; margin:0 0 6px}
.grid-h2{margin-top:52px}
.tool-grid .tool-card{
  position:relative; display:flex; flex-direction:column; gap:6px;
  padding:20px 22px 20px; border:1px solid var(--line); border-radius:var(--r);
  background:linear-gradient(180deg, var(--panel-hi), var(--panel));
  backdrop-filter:blur(8px); -webkit-backdrop-filter:blur(8px);
  text-decoration:none; box-shadow:var(--shadow-1);
  transition:border-color .18s, box-shadow .18s, transform .18s;
  overflow:hidden;
}
/* SCOPED, and it has to stay scoped. `.tool-card` is two components under one
   name (COMPONENTS.md section 1): the six-up grid card, whose base rule above
   is scoped `.tool-grid .tool-card`, and the bare `.tool-card` instrument
   panel that holds the whole workbench. These four state rules were the
   unscoped half, so every one of them reached the instrument card too.

   What that looked like, measured on the home on 2026-09-06: hovering the
   DROP ZONE lifted the entire workbench panel translateY(-3px), swapped its
   hairline border for transparent, put --shadow-2 under it
   (rgba(0,0,0,.5) 0 2px 8px, the dark language's shadow on a paper page) and
   faded this ring in at opacity 1 in rgba(61,232,160) mint and rgba(65,191,240)
   sky, two accents on a page whose one accent is amber. Nugroho reported it as
   the dropzone floating, which is what it was.

   The grid card is on /create alone now (the home dropped its six-up grid),
   so this scope costs that page nothing and the instrument card is flat. */
.tool-grid .tool-card::before{
  content:""; position:absolute; inset:0; border-radius:inherit; padding:1px;
  background:linear-gradient(140deg, rgba(61,232,160,.55), transparent 38%, transparent 62%, rgba(65,191,240,.45));
  /* Mask alpha again, not colour: the two layers punch the border out of
     the gradient so only the ring is painted. */
  -webkit-mask:linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
  -webkit-mask-composite:xor; mask-composite:exclude;
  opacity:0; transition:opacity .22s;
  pointer-events:none;
}
.tool-grid .tool-card:hover{
  border-color:transparent; box-shadow:var(--shadow-2);
  transform:translateY(-3px); text-decoration:none;
}
.tool-grid .tool-card:hover::before{opacity:1}
.tool-grid .tool-card:active{transform:translateY(-1px) scale(.99)}
.tool-ico{
  width:46px; height:46px; border-radius:12px;
  background:var(--accent-soft); border:1px solid var(--accent);
  display:grid; place-items:center; margin-bottom:6px;
  font-family:var(--mono); font-weight:600; font-size:12.5px; line-height:1;
  letter-spacing:.02em; color:var(--accent);
  transition:background .18s, color .18s, transform .18s;
}
.tool-ico svg{width:22px; height:22px}
.tool-ico.sm{font-size:10px; letter-spacing:0}
.tool-card:hover .tool-ico{background:var(--grad); color:var(--accent-ink); border-color:transparent;
  transform:translateY(-2px)}
.tool-io{
  margin-top:auto; padding-top:14px;
  font-family:var(--mono); font-size:11px; font-weight:500; letter-spacing:.1em;
  text-transform:uppercase; color:var(--muted);
}
.tool-io .arw{display:inline-block; padding:0 5px; vertical-align:-1px}
.tool-io .arw svg{width:26px; height:10px; display:block; overflow:visible}
.tool-io .arw line{stroke:var(--accent); stroke-width:2; stroke-linecap:round;
  transition:transform .2s cubic-bezier(.3,.7,.4,1)}
.tool-io .arw polygon{fill:var(--accent); transition:transform .2s cubic-bezier(.3,.7,.4,1)}
.tool-card:hover .arw line{transform:scaleX(1.45); transform-origin:left center}
.tool-card:hover .arw polygon{transform:translateX(7px)}
@media (prefers-reduced-motion:reduce){
  .tool-card:hover .arw line, .tool-card:hover .arw polygon{transition:none; transform:none}
}
.tool-name{font-family:var(--cond); font-weight:700; font-size:18px; color:var(--ink); letter-spacing:-.015em;
  transition:color .16s}
.tool-card:hover .tool-name{color:var(--accent)}
.tool-desc{font-size:14.5px; line-height:1.5; color:var(--muted)}
.lead-grid .tool-card::after{
  content:""; position:absolute; left:0; right:0; top:0; height:2.5px;
  background:var(--grad); border-radius:var(--r) var(--r) 0 0;
}
.lead-grid .tool-card{padding-top:22px}
@media (max-width:900px){ .tool-grid{grid-template-columns:repeat(2,1fr)} }
@media (max-width:560px){ .tool-grid{grid-template-columns:1fr} }

/* ------------------------------------------------------------------ footer */
/* KEEP IN STEP WITH page.css. Both stylesheets carry the footer because the
   converter pages load app.css and everything else loads page.css, and the
   footer is the one region every page on the site shares. Change one and the
   site grows two footers that are almost the same, which is worse than two
   that are obviously different because nobody notices.

   The footer stays dark in both themes, so these are literals rather than
   tokens: it is not a palette that flips, it is one surface that does not.
   margin-top is 0 here and 96px in page.css on purpose: a converter page ends
   in its own section spacing, a document page does not. */
.site-foot{
  border-top:1px solid var(--line); background:var(--foot-bg); color:var(--foot-ink);
  padding:64px 0 36px; margin-top:0; position:relative;
}
.site-foot > .wrap{position:relative}
.foot-top{
  display:grid; grid-template-columns:minmax(200px,1.15fr) minmax(0,4fr); gap:36px;
  margin-bottom:48px;
}
.foot-brand{display:flex; flex-direction:column; gap:14px}
.foot-brand .brand{color:var(--foot-hi); font-size:20px}
.foot-story{font-size:13.5px; color:var(--foot-story); line-height:1.6; max-width:30ch}
/* The mockup has four link columns and this site has five, because the footer
   is the internal-link floor: check_internal_links() fails the build if a
   landing page is not in it, and there are 29 pages to name. Five columns in
   the same width means each one is narrower than the mockup's, so the brand
   column gives up some room and the column gap tightens. Below that, the
   longest label ("Germany, XRechnung") breaks onto two lines. */
/* v2 footer: six short category columns. Each links to a /tools anchor, not
   the individual pages, so the footer stays a slim index. Mirrored from
   page.css, keep both sheets in step. */
.foot-cols{display:grid; grid-template-columns:repeat(6,minmax(0,1fr)); gap:40px 20px}
/* locale footers carry fewer columns; the build stamps cols-N so no track
   sits empty. Mirrored from page.css, keep both sheets in step. */
.foot-cols.cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}
.foot-cols.cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}
.foot-cols.cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}
.foot-cols h3{
  font-size:11px; text-transform:uppercase; letter-spacing:.14em; color:var(--foot-head);
  margin-bottom:16px; font-weight:600; font-family:var(--mono);
}
.foot-cols ul{list-style:none; margin:0; padding:0; display:grid; gap:10px}
.foot-cols a{
  color:var(--foot-link); font-size:13.5px; line-height:1.45; transition:color .15s;
  display:inline-block; padding:1px 0;
}
.foot-cols a:hover{color:var(--foot-hi)}
/* The two halves came to 1128px inside a 1140px wrap, so a 12px gap tipped
   them onto separate lines: right at the boundary, which is the worst place to
   leave a layout. The tagline may shrink now, so they share one line at full
   width and still wrap cleanly when the viewport cannot hold both. */
.foot-base{
  border-top:1px solid rgba(255,255,255,.08); padding-top:22px;
  display:flex; justify-content:space-between; flex-wrap:wrap; gap:10px 18px;
  font-family:var(--mono); font-size:11px; color:var(--foot-legal); line-height:1.7;
}
/* The tagline needs 627px and the links 501px: 1146 with the gap, which no
   desktop width here can give them, since the wrap caps at 1140. Sharing a
   line therefore meant shrinking the tagline until it broke in two and left
   one word stranded under the rest. It takes its own row instead, one clean
   line down to 627px and a normal wrap below that. */
.foot-base > :first-child{flex:1 1 100%; min-width:0}
/* shrinkable, so on a phone the link row wraps inside itself instead of
   pushing the page into a sideways scroll at its max-content width. */
.foot-base .foot-links{flex:0 1 auto; min-width:0}
.foot-base .foot-links{display:flex; flex-wrap:wrap; gap:0 6px; align-items:baseline}
.foot-base a{color:var(--foot-legal); transition:color .15s}
.foot-base a:hover{color:var(--foot-hi)}
.foot-base .sep{opacity:.45}
.foot-base a[href^="mailto:"]{text-decoration:underline; text-underline-offset:2px}

@media (max-width:1000px){ .foot-cols{grid-template-columns:repeat(2,minmax(0,1fr))} }
@media (max-width:820px){ .foot-top{grid-template-columns:1fr} }
@media (max-width:560px){ .foot-cols{grid-template-columns:1fr} }

@media (prefers-reduced-motion:reduce){
  *{transition:none !important; animation:none !important}
  .tool-grid .tool-card:hover{transform:none}
}

/* ------------------------------------------- the grouped reading index ---*/
.grid-note{color:var(--muted); font-size:15px; margin:0 0 18px; max-width:var(--measure)}
.fam-grid{
  display:grid; grid-template-columns:repeat(3,1fr);
  gap:14px; align-items:start;
}
@media (max-width:900px){ .fam-grid{grid-template-columns:repeat(2,1fr)} }
.fam{
  border:1px solid var(--line); border-radius:var(--r); background:var(--surface);
  box-shadow:var(--shadow-1); padding:14px 4px 6px;
  transition:border-color .16s;
}
.fam:hover{border-color:var(--line-strong)}
.fam-h{
  font-family:var(--mono); font-size:10.5px; text-transform:uppercase;
  letter-spacing:.12em; font-weight:500; color:var(--muted);
  margin:0 0 6px; padding:0 14px;
}
.mini{
  display:flex; align-items:baseline; justify-content:space-between; gap:10px;
  padding:9px 14px; border-top:1px solid var(--line);
  color:var(--ink-2); font-size:14.5px;
  transition:background .14s, padding-left .14s;
}
.fam .mini:first-of-type{border-top:0}
.mini:hover{background:var(--accent-soft); color:var(--ink); text-decoration:none;
  padding-left:18px}
.mini:hover .mini-name{color:var(--ink)}
.mini-name{font-weight:600; color:var(--ink); min-width:0}
.mini-out{
  font-family:var(--mono); font-size:11px; letter-spacing:.04em;
  color:var(--muted); flex:0 0 auto; text-transform:uppercase;
}
.mini:hover .mini-out{color:var(--ink-2)}
@media (max-width:560px){
  .fam-grid{grid-template-columns:1fr}
}

.lead-grid{grid-template-columns:repeat(4,1fr)}
@media (max-width:1000px){ .lead-grid{grid-template-columns:repeat(2,1fr)} }
@media (max-width:560px){ .lead-grid{grid-template-columns:1fr} }
.pair-grid{grid-template-columns:repeat(2,1fr)}
@media (max-width:700px){ .pair-grid{grid-template-columns:1fr} }

.pair-grid + .fam-grid{margin-top:22px}

/* =========================================================================
   v2 home fidelity block, ported from the approved standalone mock.
   Applies to the home page's own hero plus three home-only sections
   (privacy shield, MCP code window, closing CTA). Scoped classes only.
   ========================================================================= */

/* Roboto returns for the stat figures and labels only, on the owner's call;
   the files were already shipped for the pre-v2 site so nothing new loads. */
@font-face{
  font-family:"Roboto"; font-style:normal; font-weight:100 900; font-display:swap;
  src:url(/fonts/roboto-v51-latin.woff2) format("woff2");
  unicode-range:U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD;
}

/* page-wide ambience: two soft aurora washes over the ground colour.
   Drawn as body backgrounds rather than fixed pseudo-elements so no stacking
   context is created and every existing z-index keeps meaning exactly what it
   meant before. */
body{
  background:
    radial-gradient(60% 42% at 82% -6%, rgba(65,191,240,.075) 0%, transparent 62%),
    radial-gradient(52% 38% at 8% 12%, rgba(61,232,160,.06) 0%, transparent 60%),
    var(--bg);
}
:root[data-theme="light"] body{
  background:
    radial-gradient(60% 42% at 82% -6%, rgba(24,155,217,.055) 0%, transparent 62%),
    radial-gradient(52% 38% at 8% 12%, rgba(15,184,120,.05) 0%, transparent 60%),
    var(--bg);
}

/* centered hero */
.hero-center{text-align:center}
.hero-center .hero-in{
  display:flex; flex-direction:column; align-items:center; gap:0;
}
.grad-text{
  background:var(--grad); -webkit-background-clip:text; background-clip:text;
  color:transparent;
}
.hero-center .hero-sub{margin:24px auto 0}
.trust-inline{
  list-style:none; margin:34px 0 0; padding:0;
  display:flex; justify-content:center; gap:14px 26px; flex-wrap:wrap;
  font-size:13.5px; color:var(--band-muted);
}
.trust-inline li{display:inline-flex; align-items:center; gap:8px}
.trust-inline li::before{
  content:""; width:14px; height:14px; background-color:var(--accent);
  -webkit-mask:var(--mark-check) center/contain no-repeat;
          mask:var(--mark-check) center/contain no-repeat;
}

/* the instrument card */
.tool-stage{position:relative; max-width:900px; margin:40px auto 0; width:100%; text-align:left}
.tool-card{
  position:relative; z-index:1; border-radius:var(--r-lg);
  background:linear-gradient(180deg, var(--card-top) 0%, var(--card-bot) 100%);
  backdrop-filter:blur(16px); -webkit-backdrop-filter:blur(16px);
  box-shadow:var(--shadow-3), 0 0 60px -18px rgba(61,232,160,.25);
  overflow:hidden;
  transition:background-color .25s ease, box-shadow .25s ease;
}
/* --card-top, --card-bot, --tab-* and --hero-* are tokens: see tokens.css. */
.tc-head{
  display:flex; align-items:center; justify-content:space-between; gap:16px;
  padding:14px 20px; border-bottom:1px solid var(--line); background:rgba(255,255,255,.02);
}
.tc-tabs{display:flex; gap:4px; background:var(--tab-track); padding:4px; border-radius:10px}
.tc-tab{
  font-size:12.5px; font-weight:500; color:var(--muted);
  padding:7px 14px; border-radius:8px;
  transition:background .18s, color .18s, transform .12s;
}
.tc-tab:hover{color:var(--ink); text-decoration:none}
/* The strip moved onto every page with a drop zone. tc-head was
   space-between; panel-head is a plain flex row, so one rule restores the same
   shape: tabs on the left, the live-engine status pushed to the right edge.
   Wrapping on narrow screens keeps the three tabs on one line instead of
   squeezing the status into them. */
.panel-head .tc-tabs{margin-right:auto}
@media (max-width:560px){ .panel-head{flex-wrap:wrap} }
.tc-tab:active{transform:scale(.96)}
.tc-tab.active{background:var(--tab-active-bg); color:var(--tab-active-ink);
  box-shadow:var(--tab-active-sh)}
.tc-status{
  display:flex; align-items:center; gap:8px;
  font-family:var(--mono); font-size:10.5px; color:var(--accent); letter-spacing:.08em;
}
.dropzone-orb, .dz-orb{
  width:46px; height:46px; margin:0 auto 12px; color:var(--v5-amber-deep);
  display:grid; place-items:center;
}
.dz-orb svg{width:46px; height:46px}
/* No lift, no scale, no glass box behind the arrow. The mock answers a hover
   on the dropzone with exactly two properties, border-colour and background,
   and the icon does not move at all: an arrow that springs up 5px and grows
   5% is the dark language's idea of feedback, and it was still running on
   every page after the port. */
/* Capped and centred so the fourteen formats break 7 and 7. At the card's full
   864px they came to exactly 900 and wrapped a single CSV onto its own line,
   which read as an accident rather than a list. */
.fmt-cloud{
  display:flex; flex-wrap:wrap; justify-content:center; gap:6px;
  max-width:520px; margin:0 auto; padding:12px 18px 0;
}
.fmt-cloud span{
  font-family:var(--mono); font-size:10.5px; font-weight:500; color:var(--muted);
  border:1px solid var(--line); border-radius:999px; padding:5px 11px;
  transition:border-color .15s, color .15s, transform .15s;
}
.fmt-cloud span:hover{border-color:var(--accent); color:var(--accent); transform:translateY(-1px)}
@media (max-width:680px){
  .tc-head{flex-direction:column; align-items:stretch}
  .tc-status{justify-content:center}
}

/* stats band, three cells only, figures and labels in Roboto per owner call.
   In dark it stays a glass strip on the band; in light the panel goes solid
   white with its own ink, because translucent-white over the dark band plus
   the pale band-muted label was unreadable. */
.stats-band{
  position:relative;
  margin-top:32px; width:100%;
  display:grid; grid-template-columns:repeat(3,1fr);
  border:1px solid var(--line); border-radius:var(--r-lg);
  background:color-mix(in srgb, var(--surface) 55%, transparent);
  backdrop-filter:blur(10px); -webkit-backdrop-filter:blur(10px);
  box-shadow:var(--shadow-1);
  overflow:hidden;
}
/* the one gradient rule, same as the proof strip and every raised card */
.stats-band::before{
  content:""; position:absolute; left:0; right:0; top:0; height:2.5px;
  background:var(--grad);
}
.stat{
  padding:22px 14px 20px; display:flex; flex-direction:column; align-items:center; gap:7px;
  border-left:1px solid var(--line);
}
.stat:first-child{border-left:none}
.stat .n{
  font-family:var(--mono); font-weight:600; font-size:30px; letter-spacing:-.02em;
  font-variant-numeric:tabular-nums;
  background:var(--grad); -webkit-background-clip:text; background-clip:text; color:transparent;
  display:inline-block;
}
/* counter change pop, triggered by fx.js on #convCount */
@keyframes statPop{0%{transform:scale(1)}40%{transform:scale(1.16)}100%{transform:scale(1)}}
.stat .n.pop{animation:statPop .45s cubic-bezier(.3,.8,.4,1)}
.stat .l{
  font-family:var(--mono);
  font-size:11px; font-weight:500; color:var(--muted);
  text-transform:uppercase; letter-spacing:.1em;
}
:root[data-theme="light"] .stats-band{
  background:linear-gradient(180deg, var(--panel-hi), var(--panel));
  box-shadow:var(--shadow-2);
}
:root[data-theme="light"] .stat .l{color:var(--muted)}

/* Light mode follows the mock: the hero loses the dark band and the page is
   one pale sheet. This kills the whole class of contrast bugs where text
   pinned for the dark band (band ink, white H1) landed on cards that flip
   white in this theme and vanished. */
:root[data-theme="light"] .hero{
  background:transparent; color:var(--ink);
}
:root[data-theme="light"] .hero::before,
:root[data-theme="light"] .hero::after{content:none}
:root[data-theme="light"] .hero h1{color:var(--ink)}
:root[data-theme="light"] .hero-sub{color:var(--muted)}
:root[data-theme="light"] .hero-sub .stays{color:var(--ink)}
:root[data-theme="light"] .hero-sub a{color:var(--accent)}
:root[data-theme="light"] .trust-inline{color:var(--muted)}
:root[data-theme="light"] .marks li{color:var(--ink)}
:root[data-theme="light"] .tc-head{background:transparent}

/* section intros, as in the mock: mono tag over a display heading */
.sec-tag{
  font-family:var(--mono); font-size:11px; font-weight:600;
  letter-spacing:.14em; text-transform:uppercase; color:var(--accent);
  margin:0 0 10px;
}
.sec-sub{color:var(--muted); font-size:16px; margin:12px 0 0}
/* the intro owns the space under it too: with only a top margin it sat
   12px below its heading and flush against the cards, belonging to
   neither. .priv-list already carried its own 28px, which is where this
   number comes from. */
.sec-sub + .tool-grid,
.sec-sub + .grid-3{margin-top:28px}

/* FAQ, mock layout: centered head, bordered cards in a centered column.
   Scoped to #faq so the writer-tool pages keep their list style. */
.sec-head{text-align:center}
#faq .faq{
  max-width:780px; margin:28px auto 0; gap:0; border-top:1px solid var(--v5-line);
}
#faq .faq details{
  border:none; border-bottom:1px solid var(--v5-line); border-radius:0;
  background:none; backdrop-filter:none; -webkit-backdrop-filter:none;
}
#faq .faq summary{padding:15px 2px; font-size:16.5px}
#faq .faq details p{padding:0 2px 16px 26px; max-width:64ch}
@media (max-width:760px){
  .stats-band{grid-template-columns:1fr}
  .stat{border-left:none; border-top:1px solid var(--line)}
  .stat:first-child{border-top:none}
}

/* ---------------------------------------------------------- privacy shield */
#privacy .priv-grid{
  display:grid; grid-template-columns:1fr 1fr; gap:clamp(36px,5vw,72px); align-items:center;
}
.shield-vis{width:min(360px,86%); margin:0 auto}
.shield-vis svg{width:100%; height:auto; display:block; overflow:visible}
.orbit-ring{transform-box:view-box; transform-origin:120px 120px; animation:orbitspin 26s linear infinite}
@keyframes orbitspin{to{transform:rotate(360deg)}}
.shield-outline{stroke-dasharray:420; animation:drawshield 5.5s ease-in-out infinite}
@keyframes drawshield{0%{stroke-dashoffset:420}45%,80%{stroke-dashoffset:0}100%{stroke-dashoffset:-420}}
.scan-band{animation:scanmove 3.2s ease-in-out infinite alternate}
@keyframes scanmove{from{transform:translateY(0)}to{transform:translateY(92px)}}
.check-mark{stroke-dasharray:96; stroke-dashoffset:96; animation:drawcheck 5.5s ease-in-out infinite}
@keyframes drawcheck{0%,30%{stroke-dashoffset:96}50%,82%{stroke-dashoffset:0}100%{stroke-dashoffset:96}}
.core-glow{animation:corepulse 3s ease-in-out infinite}
@keyframes corepulse{50%{opacity:.55}}
.float-dot{animation:floaty 4s ease-in-out infinite}
.float-dot.d2{animation-delay:.8s}.float-dot.d3{animation-delay:1.6s}.float-dot.d4{animation-delay:2.4s}
@keyframes floaty{50%{transform:translateY(-9px)}}
.priv-list{list-style:none; margin:28px 0 0; padding:0; display:grid; gap:14px}
/* plain rows, as in the mock: tick, bold lead, no card box */
.priv-list li{
  display:flex; gap:13px; align-items:flex-start; font-size:15px; color:var(--ink-2);
}
.priv-list b{color:var(--ink)}
@media (max-width:900px){ #privacy .priv-grid{grid-template-columns:1fr} }

/* ------------------------------------------------------------- code window */
.mcp-grid{display:grid; grid-template-columns:1fr 1fr; gap:clamp(32px,4.5vw,60px); align-items:center}
/* THIS COMPONENT IS ON NO PAGE. Checked 2026-09-04, every .html in the tree
   including the locale folders: nothing carries .code-win, .code-body or a
   .tk-* span, and the only code that names them is the copy handler in fx.js.
   It is a terminal panel, pinned dark on a light page the way a screenshot of
   a terminal is, which is why its six inks and three syntax colours are hexes
   and not tokens. Kept rather than deleted because a dead component is the
   owner's to retire, and recorded here and in .gen/ui/COMPONENTS.md so the
   next reader does not spend the afternoon looking for the page it styles. */
.code-win{
  border:1px solid var(--line-strong); border-radius:var(--r-lg); overflow:hidden;
  background:#0B0F16; box-shadow:var(--shadow-3);
  font-family:var(--mono); font-size:12.5px; line-height:1.75;
}
.code-bar{
  display:flex; align-items:center; gap:8px; padding:12px 16px;
  border-bottom:1px solid rgba(255,255,255,.08); background:rgba(255,255,255,.02);
}
.cdot{width:11px; height:11px; border-radius:50%; flex:0 0 auto}
.code-bar span.cb-title{margin-left:8px; color:#8B97A6; font-size:11px}
.copy-btn{
  margin-left:auto; font-family:var(--mono); font-size:11px; color:#B9C4D0;
  background:rgba(255,255,255,.05); border:1px solid rgba(255,255,255,.12);
  border-radius:7px; padding:5px 12px; cursor:pointer;
  transition:color .15s, background-color .15s, border-color .15s;
}
.copy-btn:hover{color:#fff; border-color:rgba(255,255,255,.28)}
.copy-btn.copied{color:#04140D; background:var(--accent); border-color:var(--accent)}
.code-body{padding:20px 22px; color:#B7C2CF; white-space:pre; overflow-x:auto; margin:0}
.tk-k{color:#79b8ff}.tk-s{color:#3DE8A0}.tk-p{color:#c8ae7d}
@media (max-width:900px){ .mcp-grid{grid-template-columns:1fr} }

/* ------------------------------------------------------------- cta box */
.cta-box{
  position:relative; overflow:hidden; text-align:center;
  border:1px solid var(--line); border-radius:24px;
  padding:clamp(48px,6vw,78px) 30px;
  background:linear-gradient(180deg, rgba(61,232,160,.07), rgba(65,191,240,.04));
}
.cta-box::before{
  content:""; position:absolute; inset:0; pointer-events:none;
  background:radial-gradient(ellipse 55% 70% at 50% 118%, rgba(61,232,160,.18), transparent 68%);
}
.cta-box > *{position:relative}

/* mock fidelity: numbered gradient squares on the how-it-works cards */
.stepnum{
  width:34px; height:34px; border-radius:10px; margin-bottom:14px;
  background:var(--grad); color:var(--accent-ink);
  display:grid; place-items:center;
  font-weight:800; font-size:15px; font-family:var(--cond);
  box-shadow:0 0 20px rgba(61,232,160,.35);
}
/* alternating icon tints down the tool grid, mint then sky, as in the mock */
.tool-grid .tool-card:nth-child(even) .tool-ico{
  color:var(--foil); background:var(--foil-soft); border-color:var(--foil);
}

/* Hidden on screen. This sits BEFORE the print block on purpose: both
   rules are a single class, so the cascade is decided by source order,
   and with this line last it won inside the print block too and the mark
   never reached paper. Printing the page to PDF and searching it is what
   caught that; every screen check said the element was visible. */
.print-brand{display:none}

/* ---------- print ----------
   The PDF export stays the way to get a FILE, and the reasoning that chose a
   hand-rolled writer over a print stylesheet still holds: a print dialog is
   not a download. This is the other half of that job. Someone who wants paper,
   or who prints through their own PDF driver, thinks of the task as "print",
   and until now Ctrl+P gave them the site: navigation, drop zone, format
   pills, footer, and a dark background.

   THE PALETTE IS THE FIRST RULE OF PAPER, and it is in tokens.css, pinned last
   in that file so it beats both the base tokens and the [data-theme] attribute
   the toggle writes. What stays here is what only this sheet needs on paper. */

/* ---- the three national "to PDF" pages ----------------------------------

   They wear the same hero and panel as the other nine to-PDF pages now, so
   they load this sheet. Two classes they still need live only in page.css,
   and the rendered invoice itself had never had a single rule anywhere: the
   renderers emit <h1>, .header, .party, .meta, .totals and .footer into a
   container nothing styled, which is why a Polish invoice came out as a
   browser-default document with 32px headings and bare labels. */

.notice{
  display:flex; gap:11px; align-items:flex-start; margin-top:24px;
  border:1px solid var(--line); border-left:3px solid var(--accent);
  background:var(--surface); border-radius:var(--r-sm);
  box-shadow:var(--shadow-1);
  padding:13px 16px; font-size:14.5px; color:var(--ink-2);
}
.notice b{color:var(--ink)}
.block{padding:48px 0; border-top:1px solid var(--line);
  scroll-margin-top:var(--head-h)}
.page > .block:first-child{border-top:0; margin-top:0; padding-top:0}

.render-area{margin-top:24px}
/* One column, and everything in it is the width of the sheet. The three parts
   used to set their own width: the toolbar ran the full 1140px container, the
   sheet stopped at 880 and sat hard left, and the notice above them was wider
   than both, so the block read as three unrelated things stacked on a page
   with 190px of dead space down its right-hand side. --sheet is the one number
   now, and centring is the container's job rather than each part's. */
.render-area{--sheet:880px}
.render-head,
.render-toolbar,
.render-area > .invoice-render{max-width:var(--sheet); margin-left:auto; margin-right:auto}
.render-head{display:flex; flex-wrap:wrap; gap:10px; align-items:center; margin-bottom:14px}
.render-head:empty{display:none}
/* Below the sheet and to the right: a person who has just scrolled the invoice
   is at the bottom of it, and reaching for Save or Print is what they do next.
   Above the sheet the same buttons asked them to decide before they had
   looked. */
.render-toolbar{display:flex; flex-wrap:wrap; gap:10px; justify-content:flex-end; margin-top:18px}
/* The status line belongs to the same column, so it starts and ends where the
   sheet does instead of running the full container above a narrower sheet. */
.notice-col{max-width:880px; margin-left:auto; margin-right:auto}
.btn-ico{width:15px; height:15px; margin-right:7px; vertical-align:-3px}
/* A .wrap that is already inside a .wrap: it wants the max-width and the
   centring, not a second helping of the gutter. Replaces a style="padding:0"
   that sat in the render template. */
.wrap-flush{padding-left:0; padding-right:0}

/* ---- sheet mode: every page that reads an invoice ------------------------

   app.js puts .sheet-mode on body as soon as a document has been read on any
   reader page.

   Why every page. Somebody who has just dropped a file wants to know one thing
   before anything else: is this the right invoice. That is a question about
   the document, and a dark grid of its fields does not answer it. The reader
   used to open with INVOICE / SELLER AND BUYER / LINE ITEMS in dark cards on
   every page, including the home page, so the answer to "what did I just drop"
   was a table you had to read rather than a page you could recognise.

   The cards used to survive under the sheet on the pages that sell data --
   home, *-to-excel, *-to-csv, ubl-to-json, convert-invoice -- on the argument
   that those columns are what the visitor came for. They are not: what the
   visitor came for is the file the export button writes, and on screen the
   cards were the same invoice a second time, in a darker shape, directly under
   the first. Nugroho reported it as two results for one conversion, which is
   what it looked like because it is what it was.

   Explicit order on every child. Flex items default to order 0, so a card that
   was never given one would sort ahead of anything numbered. */
.sheet-mode #results{display:flex; flex-direction:column}
.sheet-mode #results > .render-head{
  order:1; display:flex; flex-wrap:wrap; gap:10px; align-items:center;
  width:100%; max-width:880px; margin:0 auto 14px;
}
/* The document comes FIRST, right under the pills. Somebody who has just
   dropped a file is looking for the invoice, not for a verdict about an
   invoice they have not seen yet. The findings sit under it. */
.sheet-mode #results > #cardRules{
  order:3; width:100%; max-width:880px; margin:18px auto 0;
}
/* width:100% as well as max-width. A flex item with auto side margins stops
   stretching, so the sheet sized itself to its content: 715px of paper under
   an 880px button row, two different left edges on one column.

   The reading copy is the same sheet in the same column and takes the same
   rule. It did not, because this selector names .invoice-render and the
   reading copy is its sibling, so it sat flush left under a centred button row
   while the preview beside it was centred: one document, two positions,
   depending on which view you were looking at. */
.sheet-mode #results > .invoice-render,
.sheet-mode #results > .xrc-host{
  order:2; width:100%; max-width:880px; margin-left:auto; margin-right:auto;
}
.sheet-mode #results > .bar{
  order:4; width:100%; max-width:880px; margin:18px auto 0;
}
/* The detail, under the document it details. */
.sheet-mode #results > .card{order:5; margin-top:16px}
/* The label keeps the chips beside it and the spacer pushes only the
   separated print button to the far end (2026-09-18: the pills are gone,
   so nothing between the chips and the print corner any more). */

/* The cards that only restate the sheet step aside, on every page rather than
   on the document pages alone. Four of them printed the invoice a second time
   in a darker shape directly under the invoice, so a conversion answered with
   two copies of one document. What is left is the three cards that are not
   restatements: the EN 16931 findings, the remittance items of an 820, which
   are not lines and never reach the sheet's table, and the raw EDI segments,
   which exist because an element's meaning is trading-partner specific and
   guessing at a label is worse than showing the segment.

   Everything those four cards held that the sheet did not is now on the sheet:
   the per-rate VAT split with its category, allowances, charges, withheld,
   rounding, prepaid, the derived-tax note, and the whole payment block. */
/* The v5 reader layout wants the doc facts card BESIDE the sheet (the mock
   facts column), so cardDoc joins the three cards that never stepped
   aside. Everything those cards held that the sheet did not is on the
   sheet: the per-rate VAT split, allowances, charges, the derived-tax
   note, and the whole payment block. */
.sheet-mode #results > .card:not(#cardRules):not(#cardSegments):not(#cardDoc){
  display:none;
}

/* ---------- the official reading copy (KoSIT) ----------

   The second view. Same sheet of paper as the preview beside it, because it is
   the same document seen another way, and an app card around it would say the
   opposite. KoSIT's own two-column reading grid: a caption on the left with
   its BT number, the value on the right.

   The toggle sits with the pills above the sheet rather than in the export
   row, for the reason recorded there: at 880px that row already carries six
   buttons and two pills and wrapped onto a second line. */
.view-toggle{display:inline-flex; gap:2px; margin-left:auto}
.view-btn{
  font:inherit; font-size:12px; line-height:1; padding:6px 10px; cursor:pointer;
  background:var(--surface-2); color:var(--ink-2);
  border:1px solid var(--line-strong); border-radius:99px;
}
.view-btn.on{background:var(--ink); color:var(--surface); border-color:var(--ink)}
.view-btn:disabled{opacity:.5; cursor:not-allowed}
.xrc-host{
  background:var(--paper-bg); color:var(--paper-ink); border-radius:12px;
  box-shadow:var(--shadow-2); padding:clamp(24px,3.4vw,44px);
  max-width:880px; font-size:14px; line-height:1.55;
  font-variant-numeric:tabular-nums;
}
.xrc-note, .xrc-wait{margin:0 0 18px; font-size:12.5px; color:var(--paper-muted)}
.xrc-tab > h2{
  font-size:15px; margin:26px 0 10px; color:var(--paper-head); font-weight:700;
  padding-bottom:6px; border-bottom:1px solid var(--paper-rule-mid);
}
.xrc-tab:first-child > h2{margin-top:0}
.xrc-section > h3{font-size:13px; margin:18px 0 6px; color:var(--paper-head); font-weight:600}
.xrc-rows{
  display:grid; grid-template-columns:minmax(140px,18rem) 1fr; gap:2px 18px; margin:0;
}
.xrc-rows dt{color:var(--paper-muted); display:flex; gap:6px; align-items:baseline}
.xrc-rows dd{margin:0; color:var(--paper-ink); overflow-wrap:anywhere}
/* The BT number, because the whole point of an official reading copy is that a
   receiver can name the field they are asking about. One-off grey: a shade
   paler than --paper-muted so the number recedes behind the label it tags. */
.xrc-bt{font-size:10.5px; color:#93a0b4; letter-spacing:.02em}
/* A nested block indents its LABELS and nothing else.

   Padding on the whole list was the first attempt, and it moved the value
   column with it: measured at 1440, values sat at x=481 on a top-level row and
   x=491 one level in, so the figures zigzagged down the sheet. One column, one
   left edge, whatever depth a field sits at; the rule on the label is what
   shows the nesting. */
.xrc-sub{margin-top:6px}
/* One-off: the nesting rail is paler than every --paper-rule-* token, which
   is the point of it. Folding it into one of them would darken the rail. */
.xrc-sub > dt{padding-left:12px; border-left:2px solid #eef2f6}
@media (max-width:560px){
  .xrc-rows{grid-template-columns:1fr; gap:0 0}
  .xrc-rows dd{margin:0 0 8px}
}

/* A sheet of paper. Light in both themes on purpose: this is the artifact the
   visitor is about to print, and showing it dark would be showing them
   something other than what comes out. */
.invoice-render{
  background:var(--paper-bg); color:var(--paper-ink); border-radius:12px;
  box-shadow:var(--shadow-2); padding:clamp(24px,3.4vw,44px);
  max-width:880px; font-size:14px; line-height:1.55;
  font-variant-numeric:tabular-nums;
}
.invoice-render h1{
  font-size:21px; line-height:1.2; letter-spacing:-.01em; margin:0 0 22px;
  color:var(--paper-head); font-weight:700;
}
.invoice-render h2{font-size:15px; margin:26px 0 8px; color:var(--paper-head)}
/* Two equal columns, seller left and buyer right, with the fact strip across
   the top: that is the order the A4 writer prints, and the preview is the
   download (CONTRACT section 1), so this file does not get to choose a nicer
   one. The rule that pins the strip to the top is further down the file, at
   `.invoice-render .header .meta`, and it has to stay later than this block. */
/* 1.30fr and the strip's 1.3fr are tuned together: they put the BUYER heading
   and the strip's second label on the same vertical edge, a little past the
   halfway line. Equal halves left the two nearly-but-not-quite aligned, which
   reads as a mistake rather than a column. */
.invoice-render .header{
  display:grid; grid-template-columns:1.30fr 1fr; gap:26px 40px; align-items:start;
  padding-bottom:20px; border-bottom:1px solid var(--paper-rule-mid);
}
.invoice-render .party strong,
.invoice-render .party b{display:block; font-size:15.5px; color:var(--paper-head)}
.invoice-render .party{font-size:13.5px; color:var(--paper-ink-2)}
/* The buyer block sits against the right edge of the sheet, the way it does on
   a paper invoice, rather than starting at the 50% line where the grid puts
   it. Two blocks of address beginning at the same distance from their own
   column edge read as one two-column list and it stops being obvious which
   party is which. The text inside stays left-aligned: it is an address, and a
   ragged-left address is hard to read. */
/* justify-self:end is gone with the two-column header: the buyer is under the
   seller now, so there is no 50% line for it to be confused with, and nothing
   pushes its longest address line into the paper's edge. */
.invoice-render .party-buyer{text-align:left}
/* ONE pair of tracks: max-content sizes to the longest label in the block, so
   every value in the column shares a left edge whatever the language does to
   the words. auto-fit was the old rule and it chose the track COUNT from the
   available width, which is why five facts wrapped 3 + 2. */
.invoice-render .meta{
  display:grid;
  /* The first value track has a floor so the second pair starts where the
     BUYER column starts, rather than wherever the first value happened to end.
     One vertical edge down the sheet instead of two that nearly agree. */
  grid-template-columns:max-content minmax(0,1.3fr) max-content minmax(0,1fr);
  gap:9px 16px; column-gap:40px;
  font-size:13.5px; color:var(--paper-ink-2); align-items:baseline;
}
.invoice-render .mk{color:var(--paper-muted)}
.invoice-render .mv{
  color:var(--paper-head); font-weight:600;
  font-variant-numeric:tabular-nums; overflow-wrap:anywhere;
}
/* The payment block runs the full width under the totals and carries up to
   seven fields, so it takes two pairs of tracks instead of one. Same cells,
   same alignment rule. */
/* ONE pair of tracks, not two. The header strip carries dates, a currency and
   a document number, all short, so two pairs use the width well. This block
   carries an IBAN, a company name and a payment term: at two pairs the value
   track was 191px, and a 22-character IBAN needs about 200, so it broke mid
   string into "...61195 / 6". A full-width value track fits all three. */
.invoice-render .meta-wide{
  grid-template-columns:max-content minmax(0,1fr);
}
@media (max-width:760px){
  .invoice-render .header{grid-template-columns:minmax(0,1fr)}
  .invoice-render .meta,
  .invoice-render .meta-wide{grid-template-columns:max-content minmax(0,1fr)}
}
.invoice-render table{width:100%; border-collapse:collapse; margin-top:18px; font-size:13.5px}
.invoice-render th{
  text-align:left; font-size:11px; letter-spacing:.07em; text-transform:uppercase;
  color:var(--paper-muted); font-weight:700; padding:0 9px 7px; border-bottom:1px solid var(--paper-rule);
}
.invoice-render td{padding:8px 9px; border-bottom:1px solid var(--paper-rule-soft); vertical-align:top}
/* Quantity, Unit, Unit price and VAT sit CENTRED under their headings. Flush
   right, a three-character quantity hung at the far edge of a wide "QUANTITY"
   column and read as though it belonged to the next column along.
   Net amount is the exception and stays right: it is the column the totals
   block underneath lines up against, and breaking that would put the line
   figures and the sum they add to on two different edges. */
.invoice-render th:nth-child(n+3):nth-child(-n+6),
.invoice-render td:nth-child(n+3):nth-child(-n+6){text-align:center}
.invoice-render th:nth-child(7),
.invoice-render td:nth-child(7){text-align:right}
.invoice-render .totals{display:flex; justify-content:flex-end; margin-top:16px}
.invoice-render .totals table{width:auto; min-width:290px; margin-top:0}
.invoice-render .totals td{border-bottom:0; padding:4px 9px}
.invoice-render .totals tr:last-child td{
  border-top:1px solid var(--paper-rule); padding-top:10px; font-weight:700; font-size:15px;
}
.invoice-render .footer{
  margin-top:26px; padding-top:14px; border-top:1px solid var(--paper-rule-soft);
  font-size:12px; color:var(--paper-muted);
  /* Two ends, like every sheet the ops painter draws (owner correction
     2026-09-27): the format label at the LEFT, where the home load-sample
     sheet shows it, the branding at the RIGHT. The site was saying it three
     different ways before: the PDF export drew "Tallystick / tally-stick.com"
     in the masthead top right, level with the invoice number; the print
     stylesheet put "Processed by tally-stick.com" bottom right; the rendered
     sheets put a bilingual "Wygenerowano przez ... / Generated by ..." bottom
     centre. One shape everywhere since, now with the label on the same end as
     the ops painter's band.

     Stated rather than inherited: the renderers ship their own stylesheet with
     text-align:center in it, and only the <body> of what they produce is
     lifted onto the page, so which rule wins would otherwise depend on a
     detail two files away. Flex also beats the renderers' own centering
     without touching their stylesheets. */
  display:flex; justify-content:space-between; gap:12px;
}
/* Pro pays for a sheet without our name on it, and that has to be true of the
   sheet on screen as well as the paper. The .print-brand rule below has said
   so inside @media print for a long time; this is the same promise for the
   rendered invoice, which IS the artifact on a to-PDF page. data-pro is set by
   paywall.js whenever the entitlement changes. The label stays: it is a fact
   about the file, not a mark. */
:root[data-pro="1"] .invoice-render .footer .foot-brand{display:none}
/* ===================================================================== fit ==
   THE PREVIEW IS A DOCUMENT, SO ON A PHONE IT IS FITTED, NOT REFLOWED.

   This sheet is what the visitor judges before deciding to download, and the
   file they download is the desktop arrangement. Reflowing the sheet on a
   phone therefore showed them something that was not the thing on offer:
   the buyer block dropped under the seller, the meta strip went to one column
   and the table grew its own sideways scroll. Measured 2026-09-01 on every
   page that renders an invoice, 66 of 66, and the owner's verdict on the live
   result was that it did not match the desktop preview at all.

   So the sheet keeps its 880px arrangement at every screen size and is scaled
   down to fit instead, which is how every document viewer presents a page.
   The page's viewport tag allows pinch zoom, so reading the detail is the
   normal document gesture rather than something this CSS has to solve.

   zoom rather than transform: zoom takes part in layout, so the sheet's box
   shrinks with it and there is no dead space underneath to correct. The steps
   are tuned to real device widths and verified by measurement, not guessed;
   each one leaves the sheet just inside the viewport.
   ========================================================================= */
@media (max-width:900px){
  .invoice-render{
    width:880px; max-width:none;
    overflow-x:visible;
    margin-left:0; margin-right:0;
  }
  .render-area > .invoice-render{margin-left:0; margin-right:0}
}
/* Each step is computed for the NARROWEST screen in its own band, against the
   FULL viewport: the sheet reclaims the 22px the wrap takes off each side with
   a negative margin, so on a phone the document runs edge to edge the way a
   PDF viewer shows a page. The first version kept the gutter and left the
   sheet floating as a small card in a wide dark surround, which is what the
   owner meant by the preview looking stunted.

   The negative margin is stated per step because it lives in the sheet's OWN
   coordinate space, which zoom has already scaled: to cancel a 22px page
   gutter at zoom z the sheet needs 22/z. Those are the numbers below. Change a
   zoom value and its margin has to be recomputed with it. */
/* The gutter is taken off the CONTAINER, not fought on the sheet. A negative
   margin on .invoice-render was tried first and never applied: two rules in
   this file, `.render-area > .invoice-render` and
   `.sheet-mode #results > .invoice-render`, both carry more specificity and
   both set the margin themselves, so the sheet stayed 6px over the edge on the
   thirteen "to PDF" pages. Widening the container has no such contest, and it
   is what makes the sheet full bleed on every page at once. */
@media (max-width:900px){
  .render-area{margin-left:-22px; margin-right:-22px}
  /* Edge to edge, so the corners and the drop shadow of a floating card do not
     draw a box around something that is meant to read as a page. */
  .invoice-render{border-radius:0; box-shadow:none}
}
@media (max-width:900px){ .invoice-render{zoom:.93} }
@media (max-width:820px){ .invoice-render{zoom:.86} }
@media (max-width:760px){ .invoice-render{zoom:.77} }
@media (max-width:680px){ .invoice-render{zoom:.68} }
@media (max-width:600px){ .invoice-render{zoom:.59} }
@media (max-width:520px){ .invoice-render{zoom:.50} }
@media (max-width:440px){ .invoice-render{zoom:.45} }
@media (max-width:400px){ .invoice-render{zoom:.41} }
@media (max-width:360px){ .invoice-render{zoom:.36} }

/* ------------------------------------------------------ the preview IS it --
   When app.js manages to draw the real page, the sheet stops being a styled
   HTML document and becomes a frame around a canvas. Everything this file does
   to .invoice-render, the padding, the white ground, the zoom steps that fit a
   reflowed sheet to a phone, has to stand down: the canvas is already drawn at
   the width it needs, and zooming it a second time would blur it.

   color-scheme:only light is the documented opt out from Chrome's Auto Dark
   Theme on Android, applied to this element alone so the rest of the site
   keeps its dark chrome. It does not bind Samsung Internet, which overrides
   pages regardless; the canvas being one flat bitmap is the better defence
   there, and is the thing to verify on a Samsung device. */
.invoice-render.pdf-mode{
  zoom:1; padding:0; width:auto; max-width:none;
  background:transparent; box-shadow:none; border-radius:0;
  color-scheme:only light;
}
/* The preview is scrolled to by the app and by anchor links; without this the
   sticky site header lands on top of the invoice masthead it was asked to
   show. */
.invoice-render, .pdf-page{ scroll-margin-top: calc(var(--head-h) + 12px); }
.pdf-page{
  display:block; width:100%; height:auto;
  background:var(--paper-bg); box-shadow:var(--shadow-2); border-radius:6px;
}
.pdf-page + .pdf-page{margin-top:14px}
@media (max-width:900px){ .pdf-page{border-radius:0; box-shadow:none} }

/* ------------------------------------------------- preview mirrors the PDF --
   The preview and the downloaded PDF were two different documents drawn by two
   different pieces of code: this sheet comes from the HTML renderer in app.js,
   the file comes from the A4 writer in zugferd-pdf.js. Someone deciding whether
   to download was therefore judging something other than what they would get.

   Showing the real PDF instead would be the only exact answer, and it is the
   wrong trade here: pdf.js is fetched from a CDN and only when OCR needs it, so
   rendering every preview through it would break the "works offline after load"
   promise the site makes on every page. These rules bring the HTML to the same
   reading order and the same furniture as the A4 writer instead. */
.invoice-render h1{
  /* One-off: half a step darker than --paper-rule, because the title rule
     carries the whole sheet and the table rules sit under it. */
  border-bottom:1px solid #cfd6df; padding-bottom:10px; margin-bottom:18px;
}
/* The writer puts the dates and the reference across the top, above the two
   parties. The renderers emit party, meta, party, so this is ordering only and
   no renderer has to change. */
/* SELLER and BUYER, the way the writer sets them. */
.invoice-render .party h3{
  text-transform:uppercase; letter-spacing:.08em; font-size:11px;
  color:var(--paper-muted); margin-bottom:4px;
}
/* The writer bands its column heads. */
/* The writer states the payment lines plainly under the totals rather than
   giving them a section heading of their own. */
.invoice-render h2{
  font-size:11px; text-transform:uppercase; letter-spacing:.08em;
  color:var(--paper-muted); font-weight:700; margin:22px 0 6px;
}

/* One-off: cell ink, a shade off --paper-ink. The two were never the same
   value and making them one is a visible change, so it is a redesign's. */
.invoice-render td{color:#1d2634; background:transparent}
.invoice-render tbody tr:hover td{background:transparent}
.invoice-render tbody tr:last-child td{border-bottom:1px solid var(--paper-rule-soft)}
.invoice-render thead th{
  /* Banded, the way the A4 writer bands its column heads. This said
     transparent and, sitting after the mirror block, quietly won. One-off:
     a fill, not a rule, and paler than any --paper-rule-* token. */
  background:#f1f3f6; position:static; font-family:inherit; font-weight:700;
  color:var(--paper-muted); font-size:11px; letter-spacing:.07em;
  padding-top:7px; padding-bottom:7px;
}
.invoice-render td.idx, .invoice-render td:first-child{color:var(--paper-muted); font-weight:400}
.invoice-render .totals tr:last-child td{border-bottom:0; color:var(--paper-head)}

/* The three renderers emit party, meta, party in that order. The ordering used
   to put the strip BENEATH the two parties; the A4 writer puts it across the
   top, above them, and the preview follows the writer now. Set here rather
   than earlier in the file because a later rule of equal weight wins, and the
   earlier attempt was silently overridden. */
.invoice-render .header .party{order:1}
.invoice-render .header .meta{order:0; grid-column:1/-1; padding-top:0; padding-bottom:14px}

/* The eight-column line-item table used to get its own sideways scroll here,
   along with a narrower padding and a one-column meta strip. All three were
   ways of reflowing the sheet to the phone, and the fit block above replaces
   them: the sheet is scaled whole, so the table arrives at the same width it
   has on the downloaded file and nothing inside it needs to scroll on its own.
   Do not put these back without removing the zoom steps, or the sheet will be
   reflowed and scaled at the same time. */

/* Printing is the whole point of these three pages. The print block above
   already forces a light canvas and hides the header, footer, hero and the
   marketing sections, and it gives every .wrap the 4mm safety margin, so only
   what it does not name is listed here. */

/* =========================================================================
   v5, the workbench language (mock-v5-home, owner-approved 2026-09-05)

   Values here are COPIED FROM mock-v5-home.html. Its :root spacing scale is
   4-pt (8/12/16/24/32/48/72) and is written out as literals, because the live
   sheets carry no spacing scale of their own.

   WHY THIS BLOCK IS WRITTEN THE WAY IT IS
   ---------------------------------------
   The first port of this block was produced by adding the reader scope to
   each home rule mechanically, which turned every

       body[data-tool="home"] .drop-title{font-size:24px}

   into

       body[data-tool="home"],
       body[data-tool]:not([data-tool="home"]) .drop-title{font-size:24px}

   and a comma at the top level of a selector list starts a NEW selector. The
   first one was the bare home body, so `.drop-title`'s 24px, `.dz-orb`'s 44px
   width and `.v5-micro`'s uppercase all landed on <body> itself. The home
   rendered 44px wide, in uppercase, at 13px. Nothing failed; the page just
   broke.

   The shared rules carry no scope at all now. Every page that loads app.css
   is v5: the English home, the reader pages in five languages, the four
   locale homes and /create. A scope that excludes nothing is not a scope, and
   the one page it could not name (/create, a bare <body> with no data-tool)
   is exactly the page that shipped in the dark language. What cannot be
   written cannot be written wrong.

   Where a rule really does belong to one family it says so directly, with no
   comma: body[data-tool="home"] for the home, body[data-tool]:not(...) for
   the readers. The home scope carried :lang(en) while the locale homes were
   still built from _tool.html; they are built from _home.html now, so all
   five homes are one page in five languages and the scope names all five. check_scope_leak() in .gen/build.py fails the build if a bare
   body selector ever shares a selector list with a descendant one again.

   Three sections, in order:
     (a) the island tokens
     (b) components every family wears
     (c) home-only and reader-only rules

   The island works the way page.css's .form-wrap island does: it re-declares
   the STANDARD tokens at light values, so every existing component (cards,
   bar, pills, drop) follows without being restyled one by one. Light is a
   decision, not a theme member: the scope beats :root and :root[data-theme]
   on specificity, so the toggle does not flip these pages, and the toggle is
   stood down on them.
   ========================================================================= */
@font-face{
  font-family:"Archivo"; font-style:normal; font-weight:500 600; font-display:swap;
  src:url(/fonts/archivo-latin.woff2) format("woff2");
  unicode-range:U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD;
}
@font-face{
  font-family:"Fraunces"; font-style:normal; font-weight:100 900; font-display:swap;
  src:url(/fonts/fraunces-latin.woff2?v=2) format("woff2");
  unicode-range:U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD;
}

/* =========================================================================
   (a) THE ISLAND. One rule, both scopes, tokens only.
   ========================================================================= */
body{
  --bg:var(--v5-bg); --surface:var(--v5-surface); --surface-2:var(--v5-sand);
  --ink:var(--v5-ink); --ink-2:var(--v5-ink-2); --muted:var(--v5-muted);
  --line:var(--v5-line); --line-strong:var(--v5-line-strong);
  --accent:var(--v5-amber); --accent-deep:var(--v5-amber-deep);
  --accent-ink:var(--v5-ink); --accent-soft:rgba(227,154,46,.14);
  --grad:none; --foil:var(--v5-amber-deep); --foil-soft:rgba(184,119,22,.10);
  --ok:#0B7A50; --head-glass:rgba(243,245,247,.90);
  --row-hover:rgba(23,33,43,.035);
  /* the hero paints itself with the band tokens; re-bind the trio so the
     hero reads dark-on-light without overriding its rules one by one */
  --band:var(--v5-bg); --band-2:var(--v5-sand);
  --band-ink:var(--v5-ink); --band-muted:var(--v5-muted); --band-hi:var(--v5-ink);
  --head-h:64px;
  color-scheme:only light;
  background:var(--v5-bg); color:var(--v5-ink);
  font-size:16px; line-height:1.5;
}

/* =========================================================================
   (b) SHARED COMPONENTS. Everything under the v5 scope, home and reader.
   ========================================================================= */
/* Links are INK with an amber hover, never amber text.

   --accent is a FILL. Dark ink on an amber button is about 8:1 and reads
   beautifully; the same amber as text on paper is 2.27:1 and is the worst
   contrast on the site. app.css:85 says a{color:var(--accent)}, which was
   right when --accent was mint on near-black, and became wrong the moment
   this island rebound --accent to amber for every page. Amber stays the one
   accent; it just stops being a text colour. */
a{color:var(--v5-ink)}
/* Hover moves the amber to the UNDERLINE and leaves the text in ink. Amber as
   text does not clear 4.5:1 on this paper in ANY shade that still reads as
   amber: #E39A2E is 2.27, --v5-amber-deep 3.57, and it only passes at #8A5910,
   which is brown. A hover state is text too, so "it is only on hover" does not
   exempt it. This is the treatment mock-v5 already uses for .page-intro p a. */
a:hover{
  color:var(--v5-ink); text-decoration:underline;
  text-decoration-color:var(--v5-amber); text-underline-offset:3px;
}

.aura{
  background:radial-gradient(1100px 480px at 70% -10%, rgba(227,154,46,.07), transparent 62%), var(--v5-bg);
}
.aura .orb,
.gridlines{display:none}

/* the hero band: flat paper. The dark language paints two aurora orbs and a
   grid on ::before/::after, and v5 forbids both, so they are stood down here
   rather than left to tint the page a measured (240,247,243) green. */
.hero{
  background:var(--v5-bg); border-bottom:none; overflow:visible;
}
.hero::before,
.hero::after{
  content:none;
}

/* header, to the mock: plain language row, a command pill, no toggle, and
   the burger only once the nav is gone */
.site-head{
  background:rgba(243,245,247,.92); border-bottom-color:var(--v5-line);
}
.site-nav a{
  color:var(--v5-ink); font-weight:500; font-size:16px;
}
.site-nav a:hover{
  color:var(--v5-ink); text-decoration:underline;
  text-decoration-color:var(--v5-amber); text-underline-offset:5px;
}
.brand{color:var(--v5-ink)}
/* The language picker is one popup, not a chip row. Native details, the same
   disclosure the burger uses, so it opens on touch and needs no script. The
   summary is one compact chip at every width; the menu reuses the panel
   recipe the burger established. */
.lang-pick{position:relative; order:1}
.lang-pick > summary{
  list-style:none; cursor:pointer; user-select:none;
  display:inline-flex; align-items:center; gap:6px;
  font-size:12.5px; font-weight:600; letter-spacing:.02em;
  color:var(--v5-muted); border:1px solid var(--line);
  border-radius:8px; padding:7px 10px; background:none;
  transition:color .15s, border-color .15s;
}
.lang-pick > summary::-webkit-details-marker{display:none}
.lang-pick > summary:hover{color:var(--v5-ink); border-color:var(--v5-ink)}
.lang-pick[open] > summary{color:var(--v5-ink); border-color:var(--v5-amber)}
.lang-pick .chev{transition:transform .15s ease}
.lang-pick[open] .chev{transform:rotate(180deg)}
.lang-menu{
  position:absolute; right:0; top:calc(100% + 8px); z-index:80; min-width:176px;
  background:var(--surface); border:1px solid var(--line); border-radius:var(--r);
  box-shadow:var(--shadow-3); padding:6px; display:grid;
  animation:menuIn .18s cubic-bezier(.3,.9,.4,1) both;
}
.lang-menu a{
  display:flex; align-items:center; justify-content:space-between; gap:14px;
  padding:9px 11px; border-radius:var(--r-xs);
  font-size:14px; color:var(--ink-2); text-decoration:none;
  transition:background .13s, color .13s;
}
.lang-menu a:hover{background:var(--accent-soft); color:var(--ink); text-decoration:none}
.lang-menu a[aria-current]{color:var(--v5-ink); font-weight:700}
.icon-btn#theme,
.head-right .menu{display:none}
.head-cta{order:3}
@media (max-width:1020px){
  /* order:2 puts the burger to the RIGHT of the language picker
     (lang-pick carries order:1) - owner swap, 2026-09-19 mobile pass. */
  .head-right .menu{display:block; order:2}
}

/* ---- the layer frame -------------------------------------------------------
   Every button on the site wears a ring set off from its fill by a gap. It is
   an OUTLINE, not a box-shadow, for two reasons that both bit here already:
   an outline takes no space, so no row has to be re-measured, and its gap is
   genuinely transparent, so the same rule works on paper, on a card and inside
   a dialog without any of them declaring what colour is behind the button.
   A box-shadow ring would have needed a --frame-gap variable per surface, and
   .btn-primary sets box-shadow:none twice below, which would have eaten it.

   Every row that holds two buttons had its gap raised to 16px to make room:
   the ring reaches 5px past each button, so a 10px gap left them touching. */
/* Frame rings: structure by default, amber on the primary fill. Never a
   black ring on an amber button (AGENTS.md). */
.btn,.ans-chip{outline:2px solid var(--frame-ring,var(--v5-line));outline-offset:3px}
.btn:focus-visible,.ans-chip:focus-visible{outline-color:var(--v5-ink);outline-width:3px}
.btn-primary{--frame-ring:var(--v5-amber-deep)}
.btn-dark{--frame-ring:var(--v5-teal-deep)}
/* One line, no ring, on the export row under a read invoice and on the facts
   card's button (owner decision 2026-09-28): the ring beside a dark border
   read as a button stuck in focus, and six of them in a row out-shouted the
   invoice they export. The ring goes transparent rather than away, so the
   :focus-visible rule above still paints the keyboard ring in ink. */
#results .bar .btn,
.facts-col .btn{--frame-ring:transparent; border-color:var(--v5-line-strong)}
#results .bar .btn:hover,
.facts-col .btn:hover{border-color:var(--v5-ink)}

/* buttons: solid amber-orange, owner law 2026-09-17 ("tetap oren"): Pricing,
   Browse and Convert stay orange. No gradient, no shine sweep. */
.btn-primary{
  background:var(--v5-amber); border-color:var(--v5-amber); color:var(--v5-ink);
  box-shadow:none; border-radius:5px;
}
/* (0,3,0), matching `.btn.btn-primary:hover` up at app.css:383, which sets
   background:var(--grad). This island declares --grad:none, so that rule
   painted the hover background TRANSPARENT while the old v5 rule below set the
   label to --v5-bg, paper white: the button's text vanished on hover. The
   label stays --v5-ink: dark ink on amber is 6.7:1. */
.btn.btn-primary:hover,
.btn.btn-primary:active{
  background:var(--v5-amber-hover); border-color:var(--v5-amber-deep);
  color:var(--v5-ink); box-shadow:none; transform:none; filter:none;
}
.btn-primary::after{display:none}
.btn-dark{
  background:var(--v5-teal); border:1px solid var(--v5-teal);
  color:var(--v5-surface); border-radius:5px;
}
.btn-dark:hover{
  background:var(--v5-teal-deep); color:var(--v5-surface);
}
.btn-link{
  color:var(--v5-ink); font-weight:600; text-decoration:underline; text-underline-offset:3px;
}
.btn-link:hover{color:var(--v5-ink); text-decoration-color:var(--v5-amber)}

/* the instrument card becomes the mock's plain panel. The card is the home
   drop's plate on every page that hosts a dropzone (owner 2026-09-25: one
   dropzone frame site-wide, card included): 20px radius, 16px rim between
   plate edge and dashed frame, the premium float. */
.tool-stage{margin:0 auto}
.tool-card{
  background:var(--v5-surface); border:1px solid var(--v5-line-strong);
  border-radius:20px;
  box-shadow:0 1px 2px rgba(23,33,43,.05), 0 24px 60px -12px rgba(23,33,43,.18);
  padding:16px;
}
/* 24px under the tabs, 8px above them: the tab strip belongs to the panel
   it labels, so the space below it (to the dropzone) has to exceed the space
   above it (to the panel edge). Refactoring UI's grouping rule, and the mock
   states the same two numbers. */
.tc-head{margin-bottom:24px; padding:8px 0 0}
.tc-tabs{
  gap:8px; border-bottom:none; background:none; padding:0; margin:0; flex-wrap:wrap;
}
.tc-tab{
  appearance:none; border:none; border-radius:0; cursor:pointer;
  font:500 16px/1 var(--sans); color:var(--v5-ink); background:none;
  padding:12px 16px; text-decoration:none;
}
.tc-tab:hover{
  color:var(--v5-ink); background:none; box-shadow:inset 0 -4px 0 var(--v5-line-strong);
}
.tc-tab.active{
  color:var(--v5-ink); font-weight:600; background:none;
  box-shadow:inset 0 -4px 0 var(--v5-amber);
}

/* The STANDARD picker rides beside the tabs on the Excel conversion pages
   (Job A): a tool control, not a marketing line, so it lives in the panel
   head, and .tc-head's space-between parks it at the right edge. Cut at
   build time on the formats with one standard (writer_body's field_cut),
   which leaves the tabs alone in the head. */
.tc-std{display:flex; align-items:center; gap:8px}
.tc-std label{font:500 13.5px/1 var(--sans); color:var(--v5-muted)}
.tc-std select{
  font:500 14px/1.2 var(--sans); color:var(--v5-ink);
  background:var(--v5-bg); border:1px solid var(--v5-line-strong);
  border-radius:5px; padding:7px 10px;
}
@media (max-width:680px){
  /* The select sizes to its longest option ("XRechnung 3.0 (Germany)"), which
     at 390px is wider than the card: without the shrink the centred row
     pushed the label out past the left edge. */
  .tc-std{flex-wrap:wrap; justify-content:center}
  .tc-std select{max-width:100%; min-width:0}
}

/* the dropzone. The frame is the home drop's frame on every page (owner
   2026-09-24: one dropzone look site-wide): the dashed amber window on the
   white plate, 14px radius. */
.drop{
  position:relative; text-align:center;
  border:1.5px dashed rgba(227,154,46,.75); border-radius:14px;
  background:var(--v5-surface); padding:32px 24px 22px;
}
.drop:hover{
  border-color:var(--v5-amber); background:var(--v5-sand);
}
/* the drag state carries the button ring grammar: a colour change AND a
   set-off ring, so "it will take this file" is a state, not a hope. Hover
   stays quiet; the ring is reserved for the drop itself. The read state
   wears the same solid amber frame without the ring (the scan band is its
   affordance), matching what the home page already did. */
.drop.over{
  border-color:var(--v5-amber); background:var(--v5-sand);
  outline:2px solid var(--v5-amber); outline-offset:4px;
}
.drop.working{
  border-style:solid; border-color:var(--v5-amber); background:var(--v5-sand);
}
/* title on top, the amber arrow centred below it (owner order 2026-09-17) */
.dz-head{
  display:flex; flex-direction:column; align-items:center; gap:12px; margin-bottom:0;
}
.dz-orb{
  width:38px; height:38px; margin:0; padding:0;
  background:none; border:none; border-radius:0; box-shadow:none;
  color:var(--v5-amber-deep);
}
.dz-orb svg{width:38px; height:38px}
.drop-title{
  font-family:var(--display); font-weight:500; font-size:21px;
  letter-spacing:-.01em; line-height:1.1; color:var(--v5-ink);
}
.drop-sub{
  color:var(--v5-muted); margin:8px auto 16px; max-width:54ch; font-size:15px;
}
.drop-actions{
  display:flex; flex-direction:column; gap:12px; align-items:center; margin-top:20px;
}
/* the owner asked for a slightly smaller Browse with air above it */
.drop-actions .btn-lg{font-size:15px; padding:11px 20px}

/* The drop zone's internal rhythm, unified with the home drop (owner
   2026-09-19: make the dropzones like the home page's). The heading block
   steps 10px, actions and the conversion strip follow at 16px; descendant
   selectors so the element margins that accumulated under the old orb-first
   markup cannot come back. The same block lives in page.css. */
.drop{padding:24px 24px 20px; margin:0}
.drop .dz-head{display:flex; flex-direction:column; align-items:center; gap:10px}
.drop .dz-orb, .drop .drop-title{margin:0}
.drop .drop-sub{margin:0 auto}
.drop .drop-actions{margin:16px 0 0}
.drop .conv-strip{margin:16px auto 0}
.under-drop{
  margin:0; font-size:15px; color:var(--v5-ink-2);
}
/* A 17px strip under a 45px primary is a thumb trap on a phone: the sample
   link measured 150x17 at 390 and misses landed on Browse files. On small
   screens the link gets a real hit area of its own; desktop keeps the bare
   underlined word. Reader pages carry the same pairing, so the rule is the
   component, not the home page. */
@media (max-width:640px){
  .under-drop .btn-link{
    display:inline-flex; align-items:center; min-height:44px; padding:8px 14px;
  }
}
/* the conversion strip, inside the drop zone on every reader page: this
   page's own pair as two document icons with a straight arrow between
   (owner 2026-09-17). Bigger than the home wall's chips because here the
   pair IS the context; icons use the build's FMT_META palette. */
/* labels are absolutely centred under their icon so a wide label cannot
   widen its cell and pull the arrow off the icon-line midpoint (owner
   2026-09-17: "tanda panah kurang ke kanan"). */
.conv-strip{
  display:flex; align-items:flex-start; justify-content:center; gap:28px;
  margin:26px 0 2px; padding-bottom:26px;
}
.conv-strip .cv{
  position:relative; display:block; width:44px; height:48px;
  font:600 13px/1.2 var(--sans); color:var(--v5-ink);
}
.conv-strip .cv svg{width:44px; height:48px; display:block}
.conv-strip .cv b{
  position:absolute; top:52px; left:50%; transform:translateX(-50%);
  white-space:nowrap; font-weight:600;
}
.conv-strip .cva{width:48px; height:24px; margin-top:12px; color:var(--v5-ink); flex:0 0 auto}
/* Premium feature row sits UNDER the hero sub, outside the drop card.
   Soft neutral icon tiles, bold claim, quiet support line. */
.feat-row{
  list-style:none; display:flex; justify-content:center; flex-wrap:wrap;
  gap:14px 56px; margin:24px 0 0; padding:0;
}
.feat-row li{
  display:inline-flex; align-items:center; gap:11px; text-align:left;
  min-width:0;
}
.feat-ico{
  width:44px; height:44px; border-radius:12px; flex:0 0 44px;
  display:grid; place-items:center;
  background:var(--v5-sand); color:var(--v5-ink-2);
}
.feat-ico svg{
  width:20px; height:20px; max-width:20px; max-height:20px;
  display:block; flex:0 0 auto; overflow:hidden;
}
.feat-copy{display:flex; flex-direction:column; gap:1px; min-width:0}
.feat-copy b{
  font-size:15px; font-weight:700; color:var(--v5-ink); line-height:1.2;
}
.feat-copy small{
  font-size:13px; color:var(--v5-muted); line-height:1.25;
}

/* format chips, the mock's .fmt pill. 6px corners put them in the card's
   radius family (10px card, 6px chip). Each chip opens with its coloured
   format badge (owner order 2026-09-16: "icon harus berwarna"), so the wall
   reads as formats, not as a word cloud. */
/* the wall reads as TWO centred rows (owner order 2026-09-17): seven chips
   on top, six below, each row centred so no empty hook is left at the end,
   and every chip hugs its own text (the X12/IIF stretch lesson). */
.fmt-cloud{display:grid; gap:10px; max-width:none; margin:16px 0 0; padding:14px 0 0}
.fmt-row{display:flex; flex-wrap:wrap; justify-content:center; gap:8px}
.fmt-cloud span,
.drop-formats span{
  display:inline-flex; align-items:center; gap:7px;
  font:500 12.5px/1 var(--sans); color:var(--v5-ink-2);
  border:1px solid var(--v5-line-strong); border-radius:6px;
  background:var(--v5-surface); padding:6px 12px 6px 7px; letter-spacing:0;
}
.fmt-cloud svg{
  width:20px; height:22px; flex:0 0 auto; display:block;
}
/* and they do not answer a hover, because they are spans: nothing here is
   clickable, so a chip that lit up amber and rose a pixel was offering an
   affordance that does not exist. The mock declares .fmt with no :hover at
   all. The one lift the design does keep is .vig:hover on the flow
   vignettes, which are links. */
.fmt-cloud span:hover,
.drop-formats span:hover{
  border-color:var(--v5-line-strong); color:var(--v5-ink-2); transform:none;
}

/* the display face across the sheet, as the mocks state it. Without this the
   pages that are neither the home nor a reader (/create and the four locale
   homes) kept app.css's condensed 800 headline, which is the dark language's
   voice: five pages in one font while nineteen were in another. */
h1, h2, h3{font-family:var(--display); font-weight:500; color:var(--v5-ink)}
h1{font-size:clamp(30px,3.4vw,44px); line-height:1.05; letter-spacing:-.02em}
h2{font-size:clamp(21px,2.4vw,28px); line-height:1.1; letter-spacing:-.02em}
h3{font-size:17px; line-height:1.25; letter-spacing:-.01em}

/* sections: 72px rhythm, uppercase micro tag, Archivo heading, 17px lead */
main > .block{padding:72px 0}
.v5-micro,
.lead-micro{
  display:block; margin-bottom:8px;
  font:600 12px/1 var(--sans); letter-spacing:.5px; text-transform:uppercase;
  color:var(--v5-muted);
}
.block h2{
  font-family:var(--display); font-weight:500; letter-spacing:-.02em;
  font-size:clamp(26px,3vw,38px); line-height:1.05; color:var(--v5-ink);
  margin:8px 0 12px;
}
.lead{
  color:var(--v5-ink-2); max-width:58ch; margin:0 0 32px; font-size:17px;
}

/* the loaded state, held to mock-v5-home. ONE design, both families.

   Until 2026-09-06 every rule below carried body[data-tool="home"], because
   the reader pages still opened with the export bar above the sheet and a
   shadowed page of paper: the home was v5 and the fifty-three reader pages
   were the dark language wearing v5 colours. The reader template now carries
   the same loaded markup part for part (bar, .startover, #sheetHead,
   #invoiceRender, aside.facts-col), so the scope excluded nothing that is
   still different and a scope that excludes nothing is not a scope. It is
   gone, which is also what stops the two families drifting into two v5s.

   Row map of the grid: sheet + facts (row 1-2), then the export row and the
   way back (rows 3-4). The sheet head keeps its row above the sheet because
   the reading-copy toggle lives in it; the pills it would otherwise carry are
   stood down, the facts card's Status row being the verdict. */
#results.on{
  display:grid; grid-template-columns:minmax(0,1fr) 320px;
  gap:24px; align-items:start; margin-top:24px;
}
/* The grid owns placement now, so the centring clamp the flex-column
   sheet-mode block puts on these four (width:100%, max-width:880px, auto side
   margins) has to come off, or a grid child is centred inside its own track.
   It was not, and the export row ran 880px from the sheet's left edge into
   the gutter under the facts column: "Processed in this tab." was cut in half
   at 950px on a 1280 viewport. Reset here rather than edited there, because
   that block is still the whole layout on any page without the grid. */
#results.on > *{grid-column:1 / -1}
#results.on > .render-head,
#results.on > .invoice-render,
#results.on > .xrc-host,
#results.on > .bar{width:auto; max-width:none; margin-left:0; margin-right:0}
/* Placement beats the `> *` reset above only by carrying the same ancestors:
   that reset is (0,3,1) and a bare `#cardDoc` is (1,0,0) on ids alone but
   loses the child combinator's context, so each of these is written as a
   child of #results.on and places as intended. Measured when it was not:
   facts card x = 150 on a 1440 viewport, the same x as the sheet, because
   grid-column 1 / -1 had won and dropped the column below the document. */
#results.on > #sheetHead{grid-column:1; grid-row:1; margin:0}
/* The sheet is a physical object on the workbench, so it gets the one
   treatment a paper document earns: a contact shadow at its edge and one
   soft ambient layer under it, both in the light language's rgba(23,33,43,…)
   family. Flat like the rest of the panel would make the document a second
   card in the grid; a heavy drop would put it back in the dark language.
   Stated at this rule's own weight, and stripped again at 900px (full bleed
   has no card to float) and in print (the block at the foot of the file
   already resets this rule by name). */
#results.on > #invoiceRender{
  grid-column:1; grid-row:2; margin:0;
  overflow:hidden;
  border:1px solid var(--v5-line-strong); border-radius:8px;
  box-shadow:0 1px 2px rgba(23,33,43,.05), 0 16px 36px -20px rgba(23,33,43,.18);
}
#results.on > .xrc-host{
  grid-column:1; grid-row:2; margin:0;
  border:1px solid var(--v5-line-strong); border-radius:8px;
  box-shadow:0 1px 2px rgba(23,33,43,.05), 0 16px 36px -20px rgba(23,33,43,.18);
}
@media (max-width:900px){
  #results.on > #invoiceRender,
  #results.on > .xrc-host{box-shadow:none}
}
/* ONE card, not two. The verdict, the format, the document's facts and the
   rule findings are one answer to one question, and splitting them across two
   bordered boxes made the second read as a separate widget that happened to be
   underneath. The column itself carries the chrome now and the cards inside it
   are plain sections divided by a hairline. */
#results.on > .facts-col{
  grid-column:2; grid-row:1 / span 2; align-self:start;
  display:flex; flex-direction:column;
  background:var(--v5-surface); border:1px solid var(--v5-line);
  border-radius:8px; overflow:hidden;
}
#results.on > .facts-col > .card{
  background:none; border:0; border-radius:0; box-shadow:none;
}
#results.on > .facts-col > .card + .card{border-top:1px solid var(--v5-line)}
#results.on .card{
  background:var(--v5-surface); border:1px solid var(--v5-line); border-radius:8px;
  box-shadow:none;
}
#cardDoc{padding:12px 18px 14px}
#cardDoc .card-head{display:none}
#cardDoc .summary{display:block; padding:0}
/* Facts are ROWS: label left and muted, value right and semibold, a hairline
   between. They were stacked label-above-value tiles, which is a card grid
   pretending to be a document, and it read as a second invoice under the
   first rather than as the margin note it is. The 2026-09-24 polish keeps
   that grammar and gives it air: taller rows so the hairlines breathe, the
   label a half step smaller than the value so the eye lands on the figure. */
#cardDoc .field{
  display:flex; flex-direction:row; justify-content:space-between; align-items:baseline;
  gap:12px; padding:11px 0; border-bottom:1px solid var(--v5-line);
}
#cardDoc .field:last-child{border-bottom:0}
#cardDoc .field[hidden]{display:none}
#cardDoc .field .k{
  font-family:var(--sans); font-size:12.5px; font-weight:400;
  text-transform:none; letter-spacing:0; color:var(--v5-muted); flex:0 0 auto;
}
#cardDoc .field .v{
  font-family:inherit; font-size:14px; font-weight:600; text-align:right;
  color:var(--v5-ink); font-variant-numeric:tabular-nums;
}
/* One accent per verdict, and only on the verdict: teal for a document that
   passed, amber for a warning, the refusal red for a failure. Nothing else in
   this column takes colour. */
#cardDoc .field.status.ok .v{color:var(--v5-teal-deep)}
#cardDoc .field.status.warn .v{color:var(--v5-amber-deep)}
#cardDoc .field.status.bad .v{color:var(--bad)}
/* The verdict reads as a state, so it carries a state mark: a small dot in
   the verdict's own colour, taken from currentColor so no second value can
   drift from the three rules above. The other rows keep their bare figures;
   the dot is what makes the first row answer "is this file fine" at a
   glance, before any of the facts is read. */
#cardDoc .field.status .v::before{
  content:""; display:inline-block; width:7px; height:7px; border-radius:99px;
  background:currentColor; margin-right:7px; vertical-align:1px;
}
/* The verdict is a sentence, so it breaks like one: balance keeps a wrap
   from stranding one short word ("here") alone on the second line of the
   good news. Values that fit on one line are untouched. */
#cardDoc .field.status .v{text-wrap:balance}
/* The findings section inside that one card. Its heading is a quiet label,
   not a second card title, because the verdict it details is already the
   Status row at the top of the same card. */
#cardRules .card-head{
  font:600 11px/1 var(--sans); letter-spacing:.08em; text-transform:uppercase;
  color:var(--v5-muted); background:none; border-bottom:0;
  padding:16px 18px 4px;
}
#cardRules .card-body{padding:4px 18px 18px}
/* The pill is stood down: it said "Passes the rules checked here" directly
   under a Status row that now says exactly that. The verdict is stated once,
   at the top of the card; this section is the detail behind it. */
#cardRules .card-head .pill{display:none}
#cardRules .rule-none{margin:0; color:var(--v5-ink-2); font-size:13.5px}
/* The findings are dressed for this column, not the wide results card they
   also sit on: the base .rule row (14.5px text, 96-110px tag track) is tuned
   for the full page width, and at 284px of column it wrapped every sentence
   into a stair. Tighter padding, a narrower tag track, text at the same size
   as the facts above it. */
#cardRules .rule{padding:9px 10px; gap:10px; grid-template-columns:minmax(84px,96px) 1fr}
#cardRules .rule-text{font-size:13px; line-height:1.45}
/* The button wraps instead of overflowing. In German the label is
   "Signierte Aufzeichnung herunterladen" and it ran 9px past the card's edge,
   where the card's own overflow clipped it. */
#cardRules .rule-sign{margin-top:12px}
#cardRules .rule-sign .btn{
  white-space:normal; text-align:left; line-height:1.35; max-width:100%;
}
#cardRules .rule-sign .muted:empty{display:none}
/* The verdict is the Status row, once. The pills stay in the markup because
   app.js writes them and moves them here, and #sheetHead stays in the flow
   because the reading-copy toggle is its other tenant, but two pills over a
   sheet that already states its verdict in the column beside it is the same
   answer twice. */
#sheetHead .pill{display:none}
/* With the pills stood down, #sheetHead holds nothing on most pages, but it
   still claimed grid row 1 and the 24px gap under it, so the sheet started
   24px below the facts card beside it and the two tops did not line up.
   It keeps its row only where it still has a tenant: the reading-copy toggle. */
#results.on > #sheetHead:not(:has(.view-toggle)){display:none}
#results.on:not(:has(.view-toggle)) > #invoiceRender,
#results.on:not(:has(.view-toggle)) > .xrc-host{grid-row:1 / span 2}
#results.on .bar{
  grid-row:3; grid-column:1/-1; margin:0;
  background:none; border:0; padding:0; justify-content:flex-start; gap:8px;
  /* and no shadow. The base .bar carries --shadow-1, which is
     rgba(0,0,0,.5) 0 1px 2px plus rgba(0,0,0,.35) 0 2px 8px: the dark
     language's drop shadow, drawn on paper. With the 12px radius still set it
     painted a floating slab around the export buttons. Resetting the
     background and the border was not enough; the shadow is what was visible. */
  box-shadow:none; border-radius:0;
}
/* The label reads again since the row became "Export to" + the document
   marks; it was stood down when it said only "Export". */
#results.on .bar .lbl{letter-spacing:.1em}
/* .receipt is gone from both templates (2026-09-06). It sat at the far right
   of the export row on margin-left:auto and ran past the row's edge, and the
   privacy promise is already made once in the hero, which is where the copy
   rule puts it. The rules are dropped rather than left styling nothing. */
#results.on > .startover{
  grid-row:4; grid-column:1/-1; justify-self:start;
}
/* The empty state steps aside while a document is on screen, the way the mock
   swaps #empty for #loaded inside the same panel. Before this the reader kept
   "Drop an invoice here" open above the invoice it had already read, so the
   page asked for a file it was in the middle of showing you. The tab row
   stays: it is navigation, not state.

   .preview-on as well as .sheet-mode, and the second class is the whole fix.
   These two used to mean the same thing: a file had been read, so the sheet
   was on screen, so the drop zone was in the way. The answer dialog split
   them. .sheet-mode now means "a file has been read" and .preview-on means
   "the visitor asked to see it", and between the two there is a real state
   nobody had styled: dialog dismissed with Escape or a click on the backdrop.
   In that state the drop zone was hidden by this rule and #results was hidden
   by the gate at the bottom of this file, so the card collapsed to 0px and the
   page showed neither surface. Measured on both families at 1280x900: #drop
   0x0 display:none, #results 0x0 display:none, .tool-card 900x0. Only a reload
   brought anything back, which is what it was reported as. */
.sheet-mode.preview-on .tool-card .drop,
.sheet-mode.preview-on .fmt-cloud{display:none}
/* and the panel around them stops being a panel. Hiding only the contents
   left a bordered box holding one tab strip and 64px of nothing, which reads
   as a component that failed to load rather than as navigation. The card's
   own chrome comes off and the strip stands on its own, which is what the
   mock shows above a loaded sheet. Same scope as the rule above: with the
   dialog dismissed the card still holds the drop zone, so it is still a panel
   and keeps its chrome. */
.sheet-mode.preview-on .tool-card{
  background:none; border:0; border-radius:0; box-shadow:none; padding:0;
}
.sheet-mode.preview-on .tool-card .tc-head{margin-bottom:0}
/* One column below 1020px: every explicit desktop row releases, or the sheet
   and the facts card would both claim row 2 and overlap. Same specificity as
   the desktop rules above, later in the file, so it wins where it matches.
   The rows become auto, so the sheet-mode order properties would decide the
   sequence again (they once put the facts card last and the export bar
   mid-page); the mock's order is stated here instead. */
@media (max-width:1020px){
  #results.on{grid-template-columns:1fr}
  #results.on > #sheetHead,
  #results.on > #invoiceRender,
  #results.on > .xrc-host,
  #results.on > .facts-col,
  #results.on .bar,
  #results.on > .startover{grid-row:auto; grid-column:1/-1}
  #results.on > #sheetHead{order:1}
  #results.on > #invoiceRender,
  #results.on > .xrc-host{order:2}
  #results.on > .facts-col{order:3}
  #results.on .bar{order:4}
  #results.on > .startover{order:5}
}

/* =========================================================================
   TOOL PAGE SECTIONS, below the workbench.

   What was here until 2026-09-06 was the old page in new colours, and it
   scored 3 of 8 on the Refactoring UI diagnostic. The four failures, each
   measured rather than felt:

     hierarchy  four sections, four h2 at the same 38px against a 44px h1.
                Nothing was primary, so the eye had no entry point.
     contrast   the body prose was --muted, 3.43:1, below the 4.5:1 floor.
     space      identical 72px on every section and a 432px dead column
                beside a 688px measure of prose.
     sameness   prose, a checklist, an accordion and a link soup all wearing
                one costume, and the accordion centred while the rest was
                left-aligned.

   Now: two sections that look like what they hold. The h2 drops to 28px so
   the h1 keeps the page. The prose keeps its 58ch measure and gains a
   companion in the column that used to be empty, and the "other formats"
   paragraph becomes the mock's .dir row list, which is how this design
   already writes a list of links on the home.
   ========================================================================= */
/* main > .block is (0,1,1) and would keep this at 72px; two classes beat it. */
section.block.tool-about{padding:64px 0}
/* No top rule here either, owner call 2026-09-09: under the closing line it
   read as a second frame around a sentence. The sibling selector repeats
   because section.block + section.block outranks the bare class on the
   reader pages, where #related follows #about. */
.tool-related,
section.block + section.block.tool-related{padding:56px 0; border-top:0}
body[data-tool]:not([data-tool="home"]) .block h2{
  font-size:clamp(20px,2.1vw,26px); line-height:1.15; letter-spacing:-.015em;
}

/* One article, not two cards. The prose is the page; the field list is a
   quiet inventory under it, same measure, same block.

   The previous layout was a 1fr 1fr grid of two surfaced, bordered, 10px
   panels. That costume said "panel" about a paragraph, and a centred
   1140px line is unreadable, so the block (not only the text) is capped
   at 65ch, inside the 45 to 75 band. The heading, the lead and the list
   share that width; the gap from heading to lead is 16px (inside the
   group) and from lead to list is 48px (between groups).

   The list is left-aligned inside the same 65ch: a list of fields is
   scanned, not read as a paragraph. No surface, no radius, no border
   around either half. Hairlines stay between the items so the inventory
   still reads as a list once the card is gone. */
.about-grid{
  display:flex; flex-direction:column; align-items:center;
  gap:48px;
}
.about-prose{
  max-width:65ch; width:100%;
  text-align:center;
}
.about-prose h2{margin:0}
.about-prose h2 + p.lead{
  margin:16px 0 0; max-width:65ch;
  font-size:18px; line-height:1.65; color:var(--v5-ink-2);
  text-align:center; text-wrap:pretty;
}
.spec{
  max-width:65ch; width:100%;
  padding:0;
}
.spec-head{
  font:600 13px/1.2 var(--sans); letter-spacing:.01em;
  color:var(--v5-muted); margin:0 0 8px; padding:0;
}
.spec-list{list-style:none; margin:0; padding:0}
.spec-list li{
  padding:10px 0; border-bottom:1px solid var(--v5-line);
  font-size:14px; line-height:1.5; color:var(--v5-ink-2);
}
.spec-list li:last-child{border-bottom:0; padding-bottom:0}

/* the way on to a sibling tool: rows, the same grammar as the home's .dir.
   It was six links inside a sentence, at --amber-deep on paper, which is
   3.57:1 and unreadable at 16px. Ink at 15.69:1 carries the link now and
   amber is kept for the hover, where it is a state and not the text. */
.related-list{
  list-style:none; margin:20px 0 0; padding:0;
  columns:2; column-gap:48px;
}
.related-list li{
  border-top:1px solid var(--v5-line); padding:10px 0;
  font-size:14.5px; break-inside:avoid;
}
.related-list li a{color:var(--v5-ink); font-weight:600; text-decoration:none}
.related-list li a:hover{color:var(--v5-ink); text-decoration:underline; text-decoration-color:var(--v5-amber); text-underline-offset:3px}
/* Quiet, without a rule: the border-top drew a line across the page under a
   tool that had already closed itself, and the owner took it off every page
   on 2026-09-09. The converter had already dropped it the same day. */
.close-note{
  margin:28px 0 0; font-size:14px; color:var(--v5-muted);
}
.close-note a{color:var(--v5-ink-2); text-decoration:underline; text-underline-offset:3px}
.close-note a:hover{color:var(--v5-ink); text-decoration:underline; text-decoration-color:var(--v5-amber); text-underline-offset:3px}

@media (max-width:900px){
  .related-list{columns:1}
}
@media (max-width:400px){
  /* 65ch already fills the wrap at this width; keep the stack, tighten
     the type and the gaps so the block still reads as one piece. */
  section.block.tool-about{padding:48px 0}
  .about-grid{gap:32px}
  .about-prose h2 + p.lead{font-size:16px; line-height:1.6}
}

/* footer, the mock's slim dark bar */
.site-foot{
  background:var(--v5-ink); border-top:1px solid var(--v5-ink); color:var(--v5-line);
  margin-top:72px; padding:32px 0;
}
.site-foot a{color:var(--v5-line)}
.site-foot a:hover{color:var(--v5-bg)}
.foot-slim .foot-brand{color:var(--v5-bg)}

/* the footer slim bar's LAYOUT is theme-neutral and unscoped on purpose: the
   markup ships to every page, and the layout has to ship with it. The colours
   above are what the v5 scope adds. This is the footer lesson from the last
   pass written as code: markup everywhere, layout in one skin, was the bug. */
.foot-slim{display:flex; align-items:center; gap:24px; flex-wrap:wrap; font-size:14px}
.foot-slim .foot-brand{font-family:var(--display); font-weight:600; font-size:15px}
.foot-slim .foot-nav{margin-left:auto; display:flex; gap:16px; flex-wrap:wrap}
/* the directory badges stack under the brand word so the pair reads as one
   phrase, Tallystick featured on these directories. The row pins one 32px
   height for every badge, which puts SaaSBrowser near the 200px width its
   own snippet asks for and scales CodeHype to the same height; each badge
   keeps its own SVG ratio. */
.foot-left{display:flex; flex-direction:column; align-items:flex-start; gap:10px}
.foot-badges{display:flex; align-items:center; gap:14px; flex-wrap:wrap}
.foot-badge img{display:block; height:32px; width:auto}

/* =========================================================================
   (c) HOME ONLY. No reader scope on any selector below this line until the
       reader block; these components exist on the home page alone.
   ========================================================================= */
body[data-tool="home"] .hero{padding:72px 0 24px; text-align:center}
body[data-tool="home"] .hero .v5-micro{
  display:flex; align-items:center; justify-content:center; gap:16px; margin-bottom:14px;
  font-size:12.5px; letter-spacing:1px;
}
body[data-tool="home"] .hero .v5-micro::before,
body[data-tool="home"] .hero .v5-micro::after{
  content:""; width:44px; height:1px; background:var(--v5-line-strong);
}
body[data-tool="home"] .hero h1{
  font-family:var(--display); font-weight:700; color:var(--v5-ink);
  font-size:clamp(38px,4.2vw,62px); line-height:1.06; letter-spacing:-.025em;
  margin:0 auto; max-width:26ch;
}
body[data-tool="home"] .hero h1 .grad-text,
body[data-tool="home"] .hero h1 .v5-mark{
  white-space:nowrap;
  color:var(--v5-amber);
  -webkit-text-fill-color:var(--v5-amber);
  text-decoration:none;
}
/* Each letter rises in, left to right. No outline, no underline bar. */
/* A space span holds a real space under pre: full width on one line, and
   the break opportunity the phone widths need when the amber line is longer
   than the screen (fr clipped 58px at 390 unwrapped). Words are wrapped in
   .mk-w so the break can only ever fall on that space, never between the
   per-letter inline-blocks inside a word (fr first wrap read "electro /
   nique"). */
body[data-tool="home"] .hero h1 .v5-mark .mk-w{display:inline-block; white-space:nowrap}
body[data-tool="home"] .hero h1 .v5-mark .mk-sp{white-space:pre}
body[data-tool="home"] .hero h1 .v5-mark .mk{
  display:inline-block;
  animation:markLetter .42s cubic-bezier(.2,.75,.25,1) both;
}
body[data-tool="home"] .hero h1 .v5-mark .mk:nth-child(1){animation-delay:.06s}
body[data-tool="home"] .hero h1 .v5-mark .mk:nth-child(2){animation-delay:.10s}
body[data-tool="home"] .hero h1 .v5-mark .mk:nth-child(3){animation-delay:.14s}
body[data-tool="home"] .hero h1 .v5-mark .mk:nth-child(4){animation-delay:.18s}
body[data-tool="home"] .hero h1 .v5-mark .mk:nth-child(5){animation-delay:.22s}
body[data-tool="home"] .hero h1 .v5-mark .mk:nth-child(6){animation-delay:.26s}
body[data-tool="home"] .hero h1 .v5-mark .mk:nth-child(7){animation-delay:.30s}
body[data-tool="home"] .hero h1 .v5-mark .mk:nth-child(8){animation-delay:.34s}
body[data-tool="home"] .hero h1 .v5-mark .mk:nth-child(9){animation-delay:.38s}
body[data-tool="home"] .hero h1 .v5-mark .mk:nth-child(10){animation-delay:.42s}
body[data-tool="home"] .hero h1 .v5-mark .mk:nth-child(11){animation-delay:.46s}
body[data-tool="home"] .hero h1 .v5-mark .mk:nth-child(12){animation-delay:.50s}
body[data-tool="home"] .hero h1 .v5-mark .mk:nth-child(n+13){animation-delay:.54s}
@keyframes markLetter{
  from{opacity:0; transform:translateY(14px) scale(.92)}
  to{opacity:1; transform:none}
}
@media (prefers-reduced-motion:reduce){
  body[data-tool="home"] .hero h1 .v5-mark .mk{animation:none}
}
body[data-tool="home"] .hero-sub{
  font-size:19px; line-height:1.35; max-width:52ch; margin:18px auto 40px;
  color:var(--v5-ink-2);
}
body[data-tool="home"] .hero-sub .stays{color:var(--v5-ink)}
body[data-tool="home"] .v5-cta{display:flex; gap:12px; justify-content:center; flex-wrap:wrap}
body[data-tool="home"] .v5-cta .btn{padding:15px 24px; font-size:16px}
body[data-tool="home"] .v5-cta .btn:active{transform:scale(.96)}
body[data-tool="home"] .v5-note{margin-top:16px; font-size:13.5px; color:var(--v5-muted)}
/* Export chips under Load a sample. Short "Export to" label, then coloured
   document tiles. No hairline above the row. */
.export-row{
  margin:12px 0 0; padding:0;
  text-align:center;
}
.export-to{
  display:block; margin:0 0 8px;
  font-size:13px; color:var(--v5-muted);
}
.export-chips{
  list-style:none; display:flex; justify-content:center; flex-wrap:wrap;
  gap:8px; margin:0; padding:0;
}
.export-chips li{
  display:inline-flex; align-items:center; gap:6px;
  font:500 13px/1 var(--sans); color:var(--v5-ink-2);
  border:1px solid var(--v5-line-strong); border-radius:6px;
  background:var(--v5-surface); padding:7px 12px 7px 8px;
}
.export-chips svg{display:block; flex:0 0 auto}

/* the workbench panel */
body[data-tool="home"] .workbench{padding:24px 0 72px}

/* the command pill in the header, home only (it opens the home palette) */
.kbd-chip{display:none}
body[data-tool="home"] .kbd-chip{
  display:inline-flex; order:2; align-items:center; justify-content:center;
  border:1px solid var(--v5-line-strong); border-radius:5px;
  background:var(--v5-surface); color:var(--v5-ink-2); cursor:pointer;
  padding:8px;
}
body[data-tool="home"] .kbd-chip svg{width:15px; height:15px; display:block}
body[data-tool="home"] .kbd-chip:hover{color:var(--v5-ink); border-color:var(--v5-muted)}
body[data-tool="home"] .kbd-chip b{font-weight:600}

/* the flow band: three numbered steps, each with a drawn scene under the
   copy (owner mock 2026-09-24, followed 1:1: number badge, title, copy,
   then the art - fanned invoice and format chips, invoice plus validation
   panel, invoice fanning out to export chips - and an arrow between cards) */
body[data-tool="home"] .flow{border-block:1px solid var(--v5-line); background:var(--v5-surface)}
body[data-tool="home"] .flow-grid{display:grid; grid-template-columns:1fr 34px 1fr 34px 1fr; align-items:stretch}
body[data-tool="home"] .flow-arrow{display:flex; align-items:center; justify-content:center}
body[data-tool="home"] .flow-arrow svg{width:22px; height:22px; color:#98A2B3; display:block}
body[data-tool="home"] .vig{
  border:1px solid var(--v5-line); border-radius:10px; background:var(--v5-surface);
  overflow:hidden; margin:0; box-shadow:0 1px 2px rgba(16,24,40,.05);
  transition:transform .15s cubic-bezier(.22,.61,.36,1), border-color .15s cubic-bezier(.22,.61,.36,1);
}
body[data-tool="home"] .vig:hover{transform:translateY(-2px); border-color:var(--v5-line-strong)}
body[data-tool="home"] .vig-body{padding:20px 20px 0}
body[data-tool="home"] .vig-head{display:flex; align-items:flex-start; gap:12px}
body[data-tool="home"] .vig-num{
  flex:0 0 auto; width:35px; height:35px; border-radius:50%;
  background:#EEF1F4; display:flex; align-items:center; justify-content:center;
  font-size:12.5px; font-weight:600; color:var(--v5-ink); font-variant-numeric:tabular-nums;
}
body[data-tool="home"] .vig-body h3{
  font-family:var(--display); font-weight:600; font-size:17px;
  letter-spacing:-.01em; margin:3px 0 0; line-height:1.3; color:var(--v5-ink);
}
body[data-tool="home"] .vig-body p{margin:10px 0 0 47px; color:#667085; font-size:13px; line-height:1.5}

/* the drawn scene each card stands on; children are placed by hand like the
   mock, the way the map chips are. Words in the art are real text so the
   locale builder can translate them. */
body[data-tool="home"] .vig-art{position:relative; height:232px; margin:16px 14px 18px}
body[data-tool="home"] .vig-doc{
  position:absolute; background:var(--v5-surface); border:1px solid #E3E8ED;
  border-radius:8px; box-shadow:0 10px 24px rgba(16,24,40,.10); padding:12px;
}
body[data-tool="home"] .vig-doc.ghost1{background:#EDF0F3; border-color:#E3E8ED; box-shadow:none}
body[data-tool="home"] .vig-doc.ghost2{background:#E7EBEF; border-color:#DDE3E9; box-shadow:none}
body[data-tool="home"] .vig-doc b{
  display:block; font-size:11px; font-weight:700; color:var(--v5-ink);
  letter-spacing:-.01em; margin:2px 0 4px;
}
body[data-tool="home"] .vig-doc .bar{display:block; height:8px; padding:0; border-radius:4px; background:#E9EDF1; margin-top:8px}
body[data-tool="home"] .vig-doc .bar.amber{background:#F2C48D}
body[data-tool="home"] .vig-doc .fpill{display:block; height:16px; padding:0; border-radius:8px; background:#EDF0F3; margin-top:10px}
body[data-tool="home"] .vig-doc .fpill.amber{background:#F5D9AF}
body[data-tool="home"] .vig-doc .drow{display:flex; align-items:center; justify-content:space-between; gap:8px; margin-top:11px}
body[data-tool="home"] .vig-doc .drow:first-of-type{margin-top:9px}
body[data-tool="home"] .vig-doc .drow em{font-style:normal; font-size:9.5px; color:#667085; white-space:nowrap}
body[data-tool="home"] .vig-doc .drow .fpill{flex:1; margin:0; min-width:0}
body[data-tool="home"] .vig-doc .drow.warn .fpill{background:#FAEFE2}
body[data-tool="home"] .vig-doc .drow .bar{flex:1; margin:0}
body[data-tool="home"] .vig-doc .drow .wtri{flex:0 0 auto; width:11px; height:11px; color:#E39A2E; display:block}
body[data-tool="home"] .vig-doc .drow.warn .bar{background:#FAEFE2; position:relative}
body[data-tool="home"] .vig-doc .drow.warn em{color:#B45309}
body[data-tool="home"] .vig-cursor{position:absolute; width:17px; height:17px; filter:drop-shadow(0 1px 1px rgba(16,24,40,.25))}
body[data-tool="home"] .vig-chip{
  position:absolute; display:flex; align-items:center; gap:7px; white-space:nowrap;
  background:var(--v5-surface); border:1px solid #E3E8ED; border-radius:8px;
  padding:4px 10px 4px 5px; font-size:12px; font-weight:600; color:var(--v5-ink);
  box-shadow:0 4px 10px rgba(16,24,40,.06);
}
body[data-tool="home"] .vig-chip .fc{
  width:19px; height:19px; border-radius:5px; flex:0 0 auto; color:#fff;
  font-size:6.6px; font-weight:700; letter-spacing:.02em;
  display:flex; align-items:center; justify-content:center;
  font-family:var(--mono, ui-monospace, SFMono-Regular, Menlo, monospace);
}
body[data-tool="home"] .vig-chip .ext{font-size:10.5px; font-weight:500; color:#98A2B3}
body[data-tool="home"] .vig-chip.more{border-style:none; box-shadow:none; background:transparent; color:#98A2B3; font-weight:500; justify-content:center; padding-left:5px}
body[data-tool="home"] .vig-drop{
  position:absolute; left:18px; bottom:6px; display:flex; align-items:center; gap:8px;
  border:1.5px dashed #C9D2DA; border-radius:10px; padding:9px 13px;
  font-size:11.5px; color:#667085; background:transparent;
}
body[data-tool="home"] .vig-drop svg{width:14px; height:14px; color:#98A2B3; display:block}
body[data-tool="home"] .vig-panel{
  position:absolute; background:var(--v5-surface); border:1px solid #E3E8ED;
  border-radius:10px; box-shadow:0 10px 24px rgba(16,24,40,.10); padding:10px 12px 6px;
}
body[data-tool="home"] .vig-panel b{display:block; font-size:11px; font-weight:700; color:var(--v5-ink); margin:0 0 2px 1px}
body[data-tool="home"] .vig-panel .prow{display:flex; align-items:center; gap:8px; padding:6px 0}
body[data-tool="home"] .vig-panel .pic{
  flex:0 0 auto; width:17px; height:17px; border-radius:50%;
  display:flex; align-items:center; justify-content:center;
}
body[data-tool="home"] .vig-panel .ok .pic{background:#16A34A}
body[data-tool="home"] .vig-panel .warn .pic{background:#E39A2E}
body[data-tool="home"] .vig-panel .pic svg{width:9px; height:9px; color:#fff; display:block}
body[data-tool="home"] .vig-panel .ptxt{display:flex; flex-direction:column; gap:1px; min-width:0}
body[data-tool="home"] .vig-panel .ptxt span{font-size:10.5px; font-weight:600; color:var(--v5-ink); line-height:1.25}
body[data-tool="home"] .vig-panel .ptxt em{font-style:normal; font-size:9.5px; line-height:1.25}
body[data-tool="home"] .vig-panel .ok .ptxt em{color:#16A34A}
body[data-tool="home"] .vig-panel .warn .ptxt em{color:#B45309}
body[data-tool="home"] .vig-links{position:absolute; inset:0; width:100%; height:100%}

/* the jobs directory: a real toolbox (owner 2026-09-17, "buat tool box
   seperti tool box"): each group is a card with a coloured pictogram square
   - the same icon family the format wall and the drop zones speak - and the
   rows stay the spec sheet under it. The amber dash is gone. */
body[data-tool="home"] .dir{display:grid; grid-template-columns:repeat(2,minmax(0,1fr)); gap:24px 32px}
body[data-tool="home"] .dir-group{
  background:var(--v5-surface); border:1px solid var(--v5-line-strong);
  border-radius:10px; padding:18px 22px 10px;
}
body[data-tool="home"] .dir-group h3{
  display:flex; align-items:center; gap:12px;
  font-size:18px; margin:0 0 8px; font-family:var(--display);
  font-weight:500; letter-spacing:-.01em; color:var(--v5-ink);
}
.dg-ico{
  width:34px; height:34px; border-radius:8px; flex:0 0 auto;
  display:inline-flex; align-items:center; justify-content:center;
}
.dg-ico svg{width:18px; height:18px; color:#fff}
body[data-tool="home"] .dir-group ul{list-style:none; margin:0; padding:0}
body[data-tool="home"] .dir-group li{
  display:flex; justify-content:space-between; align-items:baseline; gap:16px;
  border-top:1px solid var(--v5-line); padding:10px 12px; margin:0 -12px;
  border-radius:6px; font-size:14.5px;
  transition:background-color .15s cubic-bezier(.2,0,0,1);
}
body[data-tool="home"] .dir-group li:hover{background:var(--v5-sand)}
body[data-tool="home"] .dir-group li a{font-weight:600; color:var(--v5-ink)}
body[data-tool="home"] .dir-group li a:hover{color:var(--v5-ink); text-decoration:underline; text-decoration-color:var(--v5-amber); text-underline-offset:3px}
body[data-tool="home"] .dir-group li span{color:var(--v5-muted); font-size:13.5px; flex:0 0 auto; text-align:right}

/* the editorial line and the mandates card */
body[data-tool="home"] .editorial{text-align:center; padding:72px 24px}
body[data-tool="home"] .v5-quote{
  font-family:var(--display); font-weight:500;
  font-size:clamp(24px,3vw,36px); line-height:1.2; letter-spacing:-.02em;
  color:var(--v5-ink); max-width:28ch; margin:0 auto; text-transform:none;
}
body[data-tool="home"] .v5-who{
  margin-top:16px; font:600 12px/1 var(--sans); letter-spacing:.5px;
  text-transform:uppercase; color:var(--v5-muted); text-align:center;
}
/* The mandates card carries its own frame; the band separator the
   .block island draws above it ran the full viewport width under the
   quote band and read as a stray line escaping the card (owner
   screenshot 2026-09-12). The card's border is the only rule here. */
body[data-tool="home"] #mandates{border-top:0}
body[data-tool="home"] .v5-mandates{
  background:var(--v5-surface); border:1px solid var(--v5-line-strong);
  border-radius:10px; padding:24px 32px;
}
body[data-tool="home"] .v5-mandates .v5-micro{margin:0 0 14px}
body[data-tool="home"] .v5-mandates .cols{display:flex; gap:32px; flex-wrap:wrap}
body[data-tool="home"] .v5-mandates .col{flex:1 1 180px; min-width:0}
body[data-tool="home"] .v5-mandates .col b{display:block; font-size:14px; margin-bottom:2px; color:var(--v5-ink)}
body[data-tool="home"] .v5-mandates .col span{color:var(--v5-muted); font-size:13px}
body[data-tool="home"] .v5-mandates .more{margin:16px 0 0; font-size:14px}

/* the command palette */
.cmdk{display:none}
body[data-tool="home"] .cmdk{
  display:grid; position:fixed; inset:0; z-index:100;
  background:rgba(23,33,43,.42); place-items:start center;
  padding:14vh 16px 16px;
}
body[data-tool="home"] .cmdk[hidden]{display:none}
.cmdk-box{
  width:min(540px,100%); background:var(--v5-surface);
  border:1px solid var(--v5-line-strong); border-radius:10px; overflow:hidden;
}
.cmdk-box input{
  width:100%; background:transparent; border:none; border-bottom:1px solid var(--v5-line);
  color:var(--v5-ink); font:500 15.5px/1 var(--sans); padding:16px 24px;
}
.cmdk-list{max-height:300px; overflow:auto; padding:8px}
.cmdk-item{
  display:flex; width:100%; text-align:left; gap:12px; align-items:center;
  background:none; border:none; cursor:pointer; color:var(--v5-ink);
  font:500 14px/1.4 var(--sans); padding:11px 16px; border-radius:5px;
}
.cmdk-item:hover, .cmdk-item:focus-visible{background:var(--v5-sand)}
.cmdk-item small{color:var(--v5-muted); margin-left:auto; font-size:12px}
.cmdk-hint{padding:10px 24px; border-top:1px solid var(--v5-line); font-size:12px; color:var(--v5-muted)}

/* =========================================================================
   THE TOOL PAGE HEAD. Not a reader-only block any more: .hero-center is the
   mock's head grammar (micro tag, display heading, one lead, then the tool)
   and both the home and the fifty-three tool pages wear it. The home adds its
   own hero size and its two calls to action below; everything here is what
   the two share.

   What this replaced: a two-column hero with the pitch on the left and the
   drop zone squeezed into a 50% column on the right, under a panel head that
   repeated the privacy promise a third time. The tool is the page, so the
   tool gets the page's width.
   ========================================================================= */
.hero-center{padding:48px 0 24px; text-align:center}
.hero-center .hero-in{display:block}
.hero-center h1{
  font-family:var(--display); font-weight:500; color:var(--v5-ink);
  font-size:clamp(30px,3.6vw,44px); line-height:1.05; letter-spacing:-.02em;
}
/* 52ch, the mock's measure, and centred with it. The brief's ~58ch is the
   section lead's measure; a centred lead under a display heading wants the
   shorter one or the last line hangs alone. */
.hero-center .hero-sub{
  color:var(--v5-ink-2); font-size:17px; line-height:1.45;
  max-width:52ch; margin:12px auto 0;
}
.hero-center .v5-note{margin:16px 0 0; font-size:13.5px; color:var(--v5-muted)}
.hero-center .tool-stage{margin-top:32px; text-align:left}
/* The panel head and its two micro labels are gone from the template; these
   two rules stay because /support and the writer pages still carry a .panel,
   and the dot is its live marker. */
.panel{
  background:var(--v5-surface); border:1px solid var(--v5-line-strong);
  border-radius:10px; box-shadow:0 1px 2px rgba(23,33,43,.05);
}
.dot-live{background:var(--v5-teal)}
.proof-note{color:var(--v5-muted)}
/* .marks survives on the three writer-output pages (facturae/fatturapa/ksef
   to PDF), which are not built from _tool.html. Its claims are secondary text,
   so they take ink-2; without this they inherit --band-ink and read at full
   ink, one step too dark. The reader scope this rule used to carry went with
   the reader's two-column hero and .marks is on no other family, so it is
   written plainly. The companion rule that set the tick's `color` to teal is
   NOT restored: the tick is a masked ::before painted by background-color, so
   that rule changed nothing on screen for as long as it existed. */
.marks li{color:var(--v5-ink-2)}

/* =========================================================================
   RESPONSIVE
   ========================================================================= */
@media (max-width:1020px){
  body[data-tool="home"] .flow-grid{grid-template-columns:1fr; gap:20px}
  body[data-tool="home"] .flow-arrow{display:none}
  body[data-tool="home"] .dir{grid-template-columns:1fr}
  /* The results grid releases in the loaded-state block above, with the
     .facts-col and .startover rows the mock's order needs. It was restated
     here without them, and being later in the file it won: the column and the
     way back kept their desktop rows on a phone. One place now. */
}
@media (max-width:768px){
  body[data-tool="home"] .hero h1{font-size:clamp(34px,9vw,44px)}
  /* Unwrapped, the longest amber lines do not fit a phone: fr 58px, de 8px,
     it 5px past the 390 edge (measured 2026-09-19). The mark may now wrap at
     its own spaces down here; the desktop composition stays two lines, and
     the marks that fit (en, pl) never wrap because they never overflow. */
  body[data-tool="home"] .hero h1 .v5-mark{white-space:normal}
  #results.on .receipt{margin-left:0; width:100%}
}
@media (max-width:480px){
  body[data-tool="home"] .hero{padding-top:32px}
  main > .block{padding:32px 0}
  .tool-card{padding:24px 16px}
  body[data-tool="home"] .kbd-chip{display:none}
}
@media (prefers-reduced-motion:reduce){
  body[data-tool="home"] .vig{transition:none}
}

/* ---- the on-screen sheet is painted from the download's own ops -----------

   drawPdfPreview lays out nothing: it reads pdfLayoutOps' op objects and
   puts the same words at the same coordinates the serialiser writes into
   the downloaded file (CONTRACT section 1, one layout pass for both).
   Helvetica is named because the ops were measured in Helvetica; the
   white-space:pre keeps the multi-space runs the payment line and the
   footer separators carry. The paper card reuses .pdf-page, which the
   canvas pages wore, so the sheet keeps its shadow, radius and the
   mobile edge-to-edge treatment; color-scheme:only light arrives by
   inheritance from .invoice-render.pdf-mode. */
.ops-page{
  position:relative;
  display:flex; flex-direction:column; box-sizing:border-box;
  padding-bottom:var(--pad-card, 0px);
  font-family:Helvetica, Arial, sans-serif;
  color:#111;
}
.ops-page .ox{position:absolute; white-space:pre; line-height:1}
.ops-page .o-rule{position:absolute}
.ops-page .o-band{position:absolute}

/* ---- home, loaded: one framed panel, no tab strip -------------------------

   The mock holds the loaded workbench in ONE panel: surface ground, a
   line-strong frame, the sheet and the facts card inside it, and the export
   row with the way back at its feet. The live page had the export row and
   the start-over link standing naked under two bordered cards. The three
   Converter/Validator/Create buttons are gone from this page on the owner's
   order; with the strip out of the tool-card there is nothing left in it
   worth a frame of its own, so the whole stage used to step aside while a
   document was on screen. Reader pages keep their strip; this is the home
   scope. Since the answer panel moved into the card, the stage stays: the
   drop zone and the format cloud above already step aside under sheet-mode,
   and what is left in the card is the question, not an empty panel. */
body[data-tool="home"] #results.on{
  background:var(--v5-surface);
  border:1px solid var(--v5-line-strong);
  border-radius:10px;
  padding:32px;
}
@media (max-width:640px){
  body[data-tool="home"] #results.on{padding:16px}
}

/* ---- the answer panel: read first, then choose (Variant C) -----------------
   Replaces #drop in place once a file is read. Every colour resolves to a
   --v5-* token: the dark --grad/--accent-ink family must never come back
   here, it painted a mint glow on cream paper once already. */
/* ---- the dialog shell, shared by both popups ----
   440 is the decision, and the binding constraint was never the prose: it was
   the chip grid. A chip's width is now a property of the dialog rather than of
   the word inside it: 440 less 44px of padding and 24px of gaps divides into
   four 93px modules, and every chip is one module. At 400 the module came out
   at 85px and "ZUGFeRD" did not fit one, which is what forced the old row to
   size itself from its longest label. The facts line wraps to two lines now,
   which is where it belongs: it is metadata, not the headline.

   One shell, two dialogs. The sample picker wears the same box, because a
   product that asks two questions in two different shapes reads as two
   products. */
/* .42 let the hero through. Measured at 1280 with the picker open: the h1 sat
   either side of a 440px box at full weight, so the page competed with the
   question instead of receding behind it. .58 plus a 2px blur puts the page
   where a backdrop belongs, still legible as context, no longer readable as
   type. The blur is the part that does the work: dimming alone keeps the
   letterforms sharp, and sharp 90px letters draw the eye whatever their
   contrast. Both are progressive, a browser without backdrop-filter still
   gets the darker scrim. */
.dlg-scrim{position:fixed;inset:0;z-index:60;display:none;place-items:center;
  padding:24px;background:rgba(23,33,43,.58);overflow:auto;
  -webkit-backdrop-filter:blur(2px);backdrop-filter:blur(2px)}
.dlg-box{width:100%;max-width:440px;background:var(--v5-surface);
  border:1px solid var(--v5-line-strong);border-radius:12px;padding:22px;
  box-shadow:0 0 0 1px rgba(23,33,43,.05), 0 8px 20px -6px rgba(23,33,43,.18),
             0 28px 64px -16px rgba(23,33,43,.30);
  animation:dlgPop .2s cubic-bezier(.23,1,.32,1) both}
@keyframes dlgPop{from{opacity:0;transform:scale(.96) translateY(6px)}to{opacity:1;transform:none}}

#answer{display:none}
/* Answer dialog follows the sample picker's calm grammar (owner 2026-09-17):
   icon tiles, group labels only, no amber slab wall. Scoped to #answer so
   intake/workbook chips keep their sentence-sized chips. */
body.has-answers #answer,
body.has-picker #samplePicker,
body.has-intake #intakeDlg,
body.has-workbook #wbDlg{display:grid}
#answer .dlg-box{max-width:480px}
#answer .ans-verdict{
  border-left:0; border-radius:10px; background:var(--v5-sand);
  padding:12px 14px; margin-bottom:4px;
}
#answer .ans-groups{gap:0}
#answer .ans-grp{padding:14px 0}
#answer .ans-grp p{display:none}
#answer .ans-grp .ans-why{display:block}
/* Same four-across module as the sample picker. No span-2 lead tile. */
#answer .ans-row{gap:10px; row-gap:10px}
#answer .ans-chip,
#answer .ans-chip.ans-chip-2,
#answer .ans-chip.ans-lead{
  grid-column:auto;
  display:flex; flex-direction:column; align-items:center; justify-content:center;
  gap:7px; width:auto; min-height:80px; padding:12px 6px;
  background:none; border:0; border-radius:10px; box-shadow:none;
  outline:0; --frame-ring:transparent;
  color:var(--v5-ink-2);
  font-family:var(--sans); font-size:11px; font-weight:600; line-height:1.2;
  text-align:center; white-space:normal;
  transition:background-color .12s, transform .12s, color .12s;
}
#answer .ans-chip svg{width:30px; height:33px; display:block; flex:0 0 auto}
#answer .ans-chip span{display:block; max-width:100%}
#answer .ans-chip:hover{
  background:var(--v5-sand); color:var(--v5-ink);
  border-color:transparent; transform:none;
}
#answer .ans-chip:active{transform:scale(.96)}
#answer .ans-chip.ans-lead{color:var(--v5-ink); font-weight:700}
#answer .ans-chip.on{color:var(--v5-amber-deep)}
#answer .ans-chip:focus-visible{
  outline:2px solid var(--v5-amber); outline-offset:3px;
}
/* The locked DATEV tile (app.js marks it .ans-datev-locked while it is
   disabled): dimmed so it reads as locked at a glance, cursor and hover
   states neutralised so it reads as inert, not as a chip that ignored a
   click. */
#answer .ans-chip.ans-datev-locked{opacity:.45; cursor:not-allowed}
#answer .ans-chip.ans-datev-locked:hover{background:none; color:var(--v5-ink-2); transform:none}
#answer .ans-chip.ans-datev-locked:active{transform:none}
#answer .ans-over{margin-top:16px; padding-top:12px}
/* The intake dialogs' chips carry sentences, not one-word answers, so their
   rows wrap instead of sharing the answer dialog's four-across grid. */
#intakeDlg .ans-row,
#wbDlg .ans-row{display:flex; flex-wrap:wrap; gap:10px}
/* and the chips size to their word: .ans-chip stretches full width at 42px
   for the answer dialog's four-across grid, which read as a slab under a
   sentence label (owner call 2026-09-09). */
#intakeDlg .ans-chip,
#wbDlg .ans-chip{
  width:auto; min-height:0; padding:10px 16px;
  white-space:normal; line-height:1.2;
}
/* Review first renders the invoice sheets inside the dialog: a wider box
   and a scrollable stack, one paper per invoice, exactly the component the
   writer form draws. */
#intakeDlg .dlg-box{max-width:760px}
.intake-review{
  margin:14px 0 0; max-height:56vh; overflow:auto;
  border:1px solid var(--v5-line); border-radius:8px;
  background:var(--v5-bg); padding:14px;
}
.intake-cap{
  display:flex; justify-content:space-between; gap:12px;
  font:600 12.5px/1 var(--sans); color:var(--v5-ink-2); margin:0 0 8px;
}
.intake-sheet + .intake-sheet{margin-top:16px}
/* #drop stays put behind the scrim. The popup is a layer over the page, not a
   replacement for it, so dismissing it returns you to a dropzone that is still
   there. .fmt-cloud goes, because a row of formats to bring IN reads as a
   second set of choices while a dialog is asking about going OUT. */
body.has-answers .fmt-cloud{display:none}

/* the verdict: a stamp on the document, not a headline. It is reassurance,
   and reassurance that competes with the task is noise. */
.ans-verdict{border-left:3px solid var(--v5-teal);border-radius:0 7px 7px 0;
  background:var(--v5-sand);padding:11px 14px}
.ans-v1{font-weight:600;font-size:15px;color:var(--v5-ink);text-wrap:pretty}
.ans-tick{color:var(--v5-teal);font-weight:700}
.ans-tick.warn{color:var(--warn)}
.ans-v2{color:var(--v5-muted);font-size:12.5px;margin-top:3px;line-height:1.45;
  font-variant-numeric:tabular-nums;text-wrap:pretty}
/* The facts were one string joined on " · ", so the wrap fell wherever the
   width ran out: measured at 1280 it broke "totals / add up" and left the
   separator hanging at the end of line one. Each fact is its own nowrap span
   now and carries its own leading separator, so a fact never splits and a
   separator never ends a line. The break opportunity is the plain space
   BETWEEN the spans, which is why app.js joins them with one. */
.ans-fact{white-space:nowrap}
.ans-fact + .ans-fact::before{content:"\00b7\00a0";color:var(--v5-line-strong)}
/* The same recipe as .block h2 up at app.css:2157, one step down in size: the
   dialog asks a section-heading question, so it speaks in the section
   heading's voice. It was 17px with default tracking, which is neither the
   page's h2 nor its body, and read as a third typeface on a two-typeface site. */
.ans-ask{font-family:var(--display);font-weight:500;font-size:21px;line-height:1.14;
  letter-spacing:-.02em;color:var(--v5-ink);margin:22px 0 4px;text-wrap:balance}
/* Focus is moved here when the dialog opens so a screen reader announces the
   question. Chrome matches :focus-visible on that programmatic focus, so the
   ring that was meant for keyboards was painted on every open: measured with
   h2.matches(":focus-visible") === true. A heading is not a control and does
   not need one. Sighted keyboard users tab straight on to the first chip,
   which has a ring of its own. */
.ans-ask:focus,
.ans-ask:focus-visible{outline:none}
.ans-sub{color:var(--v5-muted);font-size:13.5px;margin:0 0 22px}
/* One column. Two columns existed because the panel was inline and had 1250px
   of page to fill; inside a dialog they would be 190px each and every chip row
   would wrap. */
/* A rule between the groups, not 26px of nothing. Three headings, three
   sentences and three chip rows in one 440px column read as one long list
   when only whitespace divides them, and the picker made that worse: its
   groups have no sentence at all, so the only thing between "KSeF" and "EDI
   MESSAGE" was air. The rule says where a group ends without another word.
   16px each side of it, so the pair reads tighter than the 26px gap it
   replaces even though the dialog gains a line of structure. */
.ans-groups{display:grid;grid-template-columns:1fr;gap:0;align-items:start}
.ans-grp{border:0;border-radius:0;background:none;
  padding:16px 0;border-top:1px solid var(--v5-line)}
.ans-grp:first-child{border-top:0;padding-top:0}
.ans-grp:last-child{padding-bottom:0}
/* .v5-micro's recipe exactly (app.css:2151), because this IS that component:
   the uppercase tag over a section. It was 11px at .1em, which next to the
   page's own 12px at .5px looked like a different site's small caps. */
.ans-grp h3{font:600 12px/1 var(--sans);letter-spacing:.5px;text-transform:uppercase;
  color:var(--v5-muted);margin:0 0 7px}
.ans-grp p{color:var(--v5-ink-2);font-size:13.5px;margin:0 0 18px;text-wrap:pretty}
/* A four-column module grid, not a flex row that sizes itself from its longest
   label. Flex gave "Open invoice" 110px and "Print" 58px in the same row, and
   nine chips across three rows came out at seven different widths. The grid
   makes width a property of the dialog: one module, 93px, everywhere. A label
   too long for one module takes two, and says so in the markup rather than by
   pushing its neighbours around. */
.ans-row{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));
  gap:16px;row-gap:18px}
.ans-chip-2{grid-column:span 2}
/* The quiet row: secondary exits under the primary answer. This skin's
   answer row is a grid, so the row spans all four columns where the
   page.css island's flex row uses flex-basis. page.css has carried the
   component since the intake dialogs; the reader skin needed it the day
   the workbook dialog joined the same grammar. */
.ans-quiet{grid-column:1 / -1; display:flex; flex-wrap:wrap; gap:10px; margin-top:2px}
.ans-quiet .ans-chip{background:var(--v5-sand); border-color:var(--v5-line-strong);
  --frame-ring:var(--v5-line); color:var(--v5-ink-2)}
.ans-quiet .ans-chip:hover{background:var(--v5-line); color:var(--v5-ink)}
/* 40px is the pointer floor named in the interface guidelines, not a number
   picked to fit. Thumbs get 44 back in the small-screen block below. */
/* Amber, every one of them. An outlined chip beside a filled one reads as a
   secondary action, and none of these is secondary: they are the same question
   asked six ways. The spine underneath is the one place this dialog spends any
   boldness, and it is not decoration: it is the depth the chip travels when it
   is pressed, so the click has somewhere to go.

   The modifier is .ans-lead, NOT .lead. app.css:2161 already had a .lead for
   lead paragraphs, `margin:0 0 32px`, and a chip wearing that class inherited
   the 32px: the leading chip measured 40px tall while its own row measured 72
   and every neighbour stretched to match. Measured, not guessed, at 1280px:
   Excel 62x40 beside CSV 56x72. Scope any modifier that lives on a chip. */
.ans-chip{font-family:var(--sans);font-size:12.5px;font-weight:600;line-height:1;color:var(--v5-ink);
  background:var(--v5-amber);border:1px solid var(--v5-amber-deep);border-radius:9px;
  --frame-ring:var(--v5-amber-deep);
  width:100%;padding:0 6px;min-height:42px;cursor:pointer;white-space:nowrap;
  display:inline-flex;align-items:center;justify-content:center;
  transition:background-color .12s,transform .12s}
.ans-chip:hover{background:var(--v5-amber-hover);border-color:var(--v5-amber-deep);color:var(--v5-ink)}
.ans-chip:active{transform:translateY(1px)}
.ans-chip.ans-lead{background:var(--v5-amber-hover);font-weight:700;outline-width:3px}
.ans-chip.ans-lead:hover{background:var(--v5-amber-hover)}
/* Archive is on: same quiet switch treatment as .arch-toggle.on, so the chip
   in the home answer dialog and the bar on convert-invoice read as one control. */
.ans-chip.on{background:var(--v5-surface);border-color:var(--v5-amber);color:var(--v5-amber)}
/* No line-through. A struck-out label reads as deleted; these are available,
   just not on this plan, and the note under the row says so in words. */
/* A locked chip is not a dimmed amber one. Amber is what "you can press this"
   looks like here, so draining it to sand and cutting the border to a dash is
   the difference between reading the row and clicking to find out. */
.ans-chip[disabled]{cursor:not-allowed;background:var(--v5-sand);color:var(--v5-muted);
  border:1px dashed var(--v5-line-strong);--frame-ring:var(--v5-line);transform:none}
.ans-chip[disabled]:hover{background:var(--v5-sand);border-color:var(--v5-line-strong);color:var(--v5-muted)}
/* .ans-grp .ans-why, not .ans-why: the note is a <p> inside .ans-grp, so the
   0,1,1 of `.ans-grp p` above outranked the component's own 0,1,0 and handed it
   `margin:0 0 18px`. Measured, not read: the note's top sat at 542 and the chip
   row's bottom sat at 542, so the note overlapped the frame by the 6px the ring
   reaches down. This is the second selector this dialog has lost to a rule
   written for prose; the first was .lead on the chip. Scope both ends. */
.ans-grp .ans-why{color:var(--v5-muted);font-size:12.5px;margin:20px 0 0;
  min-height:16px;text-wrap:pretty}
/* A group with nothing locked reserved 16px anyway, which opened a hole under
   the first row. Nothing shifts when the note fills: every chip that writes one
   closes the dialog in the same click. */
.ans-grp .ans-why:empty{display:none;min-height:0;margin:0}
/* Each locked chip gets its own line. Two conditions on one line read as
   one sentence about one thing, and they are not. */
.ans-note{display:block}
.ans-note + .ans-note{margin-top:3px}
/* One shell, two dialogs, and the way out has to be in the same corner in
   both. space-between put the picker's Cancel bottom right (it has a note to
   its left) and the answer's "Start over" bottom left (it has none), so the
   two halves of one component dismissed from opposite corners. flex-end pins
   the way out to the right in both; the note, when a dialog has one, is
   pushed back to the left by its own auto margin. */
.ans-over{display:flex;gap:12px;align-items:center;justify-content:flex-end;flex-wrap:wrap;
  margin-top:20px;padding-top:14px;border-top:1px solid var(--v5-line)}
.ans-over .ans-said{color:var(--v5-muted);font-size:12.5px;margin-right:auto}
/* The sample picker asks by icon, not by amber chip (owner round 2026-09-17).
   Fourteen filled slabs read as a warning wall; the coloured document icons
   the home wall and the drop zones already speak say "format" at a glance.
   Every rule here is scoped to #samplePicker or to .pick-ic, a class only the
   picker emits, because .ans-chip still serves #answer, #intakeDlg and #wbDlg.
   The tiles carry no frame of their own (owner: "tidak perlu ada frame") and
   the lead format wears no bar either (owner, same round): the bold ink label
   is the whole marker, and --frame-ring only colours its keyboard ring. */
#samplePicker .ans-row{gap:10px;row-gap:10px}
.pick-ic{display:flex;flex-direction:column;align-items:center;justify-content:center;
  gap:7px;min-height:80px;padding:12px 6px;cursor:pointer;text-align:center;
  background:none;border:0;border-radius:10px;
  font-family:var(--sans);font-size:11px;font-weight:600;line-height:1.2;color:var(--v5-ink-2);
  transition:background-color .12s,transform .12s}
.pick-ic svg{width:30px;height:33px;display:block}
.pick-ic span{display:block;max-width:100%}
.pick-ic:hover{background:var(--v5-sand);color:var(--v5-ink)}
.pick-ic:active{transform:scale(.96)}
/* The picker tile wears no ring at rest, unlike .btn and .ans-chip whose
   permanent outline is their own grammar. Keyboard focus still gets the
   shared ring, and on the lead it comes out amber through --frame-ring. */
.pick-ic:focus-visible{outline:2px solid var(--frame-ring,var(--v5-line-strong));outline-offset:3px}
.pick-ic.pick-lead{color:var(--v5-ink);font-weight:700;--frame-ring:var(--v5-amber-deep)}
/* The preview is not free: it draws a whole invoice. While the dialog is up it
   stays out of the way, so the popup is the screen, and every action closes the
   dialog before it fires. That is also what keeps Print working: it prints the
   sheet, and the sheet has to be visible when it runs. */
/* Nothing of the invoice appears until the visitor has decided something.
   Gating on .has-answers was wrong: dismissing the dialog with Escape or a
   click on the backdrop dropped that class and the whole invoice appeared, a
   render nobody asked for. The gate is now the decision itself. .preview-on is
   set by "Open invoice", by Print, and by a sample the visitor picked by
   format, and by nothing else.

   #results, not .results: .sheet-mode #results carries an id, and an id beats
   any number of classes, so the class-based version of this rule lost in
   silence. This one ties on specificity and wins on order. */
body:not(.preview-on) #results{display:none}
/* Same gate as #results. The file switcher only earns its place beside a sheet
   that is on screen; with two docs loaded and the answer dialog dismissed
   (or an export fired without Open), it used to sit as bare chips between the
   drop zone and How it works, repeating the filename with nothing to switch.
   Measured on localhost after loading the same file twice: two
   xrechnung-cii.xml OK chips above .howit, #results display:none. */
body:not(.preview-on) #files{display:none}
@media (max-width:520px){
  .dlg-scrim{padding:14px}
  .dlg-box{padding:20px;max-width:none}
  /* Four modules of 67px cannot hold "ZUGFeRD" on a 360px phone, so the grid
     halves rather than the type shrinking. A two-module chip becomes the full
     width, which is what it wanted to be here anyway. */
  .ans-row{grid-template-columns:repeat(2,minmax(0,1fr))}
  .ans-chip{min-height:44px;font-size:13px;padding:0 10px}
  .ans-ask{font-size:19px}
  /* The picker's icon grid keeps four across: a 67px tile holds a 26px icon
     and the name under it, which is how every phone launcher does it. The id
     beats the .ans-row halving above, columns included, or the picker alone
     would run seven rows tall on a phone. */
  #samplePicker .ans-row{grid-template-columns:repeat(4,minmax(0,1fr));gap:8px;row-gap:8px}
  .pick-ic{min-height:72px;padding:10px 4px;font-size:10.5px}
  .pick-ic svg{width:26px;height:29px}
}
@media (prefers-reduced-motion:reduce){
  .dlg-box{animation:none}
  .ans-chip{transition:none}
  .ans-chip:hover{transform:none}
  .ans-chip:active{transform:none}
  .pick-ic{transition:none}
  .pick-ic:active{transform:none}
}

/* ---- the reversible pair ------------------------------------------------

   Six tools here have an exact opposite and, until 2026-09-08, not one of the
   twelve pages linked to it: somebody who landed on "XRechnung to Excel"
   needing the other direction had no route but the footer. They stay two
   pages, which Search Console settled rather than taste: over the 90 days to
   that date the two directions shared no query at all, and the read side is
   the best tool page the site has.

   So this is the way ACROSS, not a merge. Quiet on purpose: it sits under the
   promise line at the same weight, because it answers a minority who arrived
   at the wrong half, and must not compete with the drop zone that answers
   everybody else. */
.swap{
  display:flex; align-items:center; justify-content:center; gap:7px;
  margin:10px 0 0; font-size:13.5px; color:var(--v5-muted);
}
.swap-ico{width:20px; height:10px; flex:0 0 auto; color:var(--v5-amber)}
.swap a{color:inherit; text-decoration:underline; text-underline-offset:3px;
        text-decoration-color:var(--v5-line-strong)}
.swap a:hover{color:var(--v5-ink); text-decoration-color:var(--v5-amber)}

/* ---------- remittance / EDI detail (screen chrome; lives ABOVE print) ----------
   Official 820 (Stedi): RMR loop is the cash-application payload - keep the
   remittance table. Raw ISA/GS/ST elements are syntax audit, not the human
   reading (KoSIT visualization leads with the semantic document, not tags).
   Compact: cap width, nowrap cells, short scroll stack. */
#cardPayments, #cardSegments{max-width:920px}
.table-compact table{width:max-content; min-width:100%}
.table-compact th,.table-compact td{padding:8px 12px; white-space:nowrap}
.table-compact td:nth-child(3){max-width:280px; overflow:hidden; text-overflow:ellipsis}
#cardSegments .table-scroll{max-height:320px; overflow:auto}
.card-fold{border:1px solid var(--line); border-radius:var(--r); background:var(--surface);
  box-shadow:var(--shadow-1); overflow:hidden; display:block}
.card-fold > summary.card-head{
  list-style:none; cursor:pointer; padding:13px 16px; border-bottom:1px solid transparent;
  background:var(--surface-2);
  font-family:var(--mono); font-size:11.5px; font-weight:600; text-transform:uppercase;
  letter-spacing:.1em; color:var(--muted);
  display:flex; align-items:center; gap:10px;
}
.card-fold > summary.card-head::-webkit-details-marker{display:none}
.card-fold > summary.card-head::after{content:"+"; margin-left:auto; font-size:14px; color:var(--muted)}
.card-fold[open] > summary.card-head{border-bottom-color:var(--line)}
.card-fold[open] > summary.card-head::after{content:"-"}
.card-fold[hidden]{display:none !important}
.sheet-mode #results > #cardPayments{display:none}

/* =========================================================================
   PAPER, and why this block is the LAST thing in the sheet.

   It used to sit in the middle, above the v5 island. @media print adds no
   specificity, so every v5 rule that named the same element at the same
   weight simply won by coming later: the sheet printed on the cream paper
   ground instead of white, the form inputs kept their 1px boxes, and the
   toolbars the block names came back. Nothing failed loudly; print just
   stopped being an override layer. Measured 2026-09-10 on /create-xrechnung
   with Chrome print emulation: body background rgb(253,251,244), input
   border-bottom 1px, with this block still in the file.

   Print is the last word about the document: append new sections ABOVE
   this line; the foot-of-file print block is the one sanctioned exception.
   ========================================================================= */

@media print{
  /* The hexes in this block are deliberate; see the same note in page.css.
     The print palette reaches an element by inheritance from :root, and a
     component that declares its own tokens for its own subtree does not
     inherit it. Ink on paper is black, so this block states black rather
     than asking a token what black is here. */
  html, body{background:#fff; color:#111}

  /* No animation on paper. The results list fades its children in with
     animation-fill-mode:both and a stagger, and `both` holds an element at its
     FROM keyframe whenever the animation is not running. Print does not run
     animations, so the last child in the list, which is the brand line, was
     held at opacity 0 and never reached the sheet. Nothing on paper moves, so
     nothing on paper needs an animation. */
  *, *::before, *::after{
    animation:none !important; transition:none !important; opacity:1;
  }

  /* Everything that is the site rather than the document.

     section.block is the landing-page copy UNDER the converter: the about
     section, the field list, the FAQ and the related-format list. Only a
     screenshot showed those printing; a computed-style check said the header
     and the hero were gone and reported nothing about four sections of
     marketing following the invoice onto the paper. */
  .aura, .gridlines, .skip, .site-head, .site-foot,
  .hero, .proof-wrap, .files, #error, section.block,
  /* The paywall is a native <dialog> with no backdrop element of its own, so
     the element is named rather than a wrapper class that does not exist.
     Archive/dialog chrome is named for the same reason: it is parked off
     <main> and re-parented to <body> at bind. */
  .bar, #datevPanel, dialog, .pw-dlg,
  .arch-bar, #archDlg, .dlg-scrim, .conv-strip{display:none !important}

  /* PARENT CONTRACT. Paper is the sheet and the provenance mark. Every other
     child of #results is shop chrome: export bar, start-over, render head,
     facts column, parties/lines/tax/payment cards, remittance, segments.
     New chrome added to the results grid hides itself on paper without a new
     selector here. The HTML floor is still a last resort if layout ops never
     produced pages; chrome never rides with it. */
  #results > *:not(#invoiceRender):not(.print-brand){display:none !important}
  #results > #invoiceRender{display:block !important}

  /* A button on paper is a dead pixel, including one nested outside the
     results grid (signed-record upsell and its note). */
  .results button, .results .btn, .rule-sign{display:none}

  /* The document itself, given the whole sheet. The 4mm is not the page
     margin, which @page sets below: it is what keeps the last column off the
     paper edge when a visitor prints with margins set to none. */
  .wrap, .wrap.tool{max-width:none; width:auto; margin:0; padding:0 4mm}
  .results{display:block}

  /* A card split across a page break is the one thing that makes a printed
     invoice hard to read, so each one is kept whole where the paper allows. */
  /* Deliberately NOT break-inside:avoid. A tall card that refuses to split
     throws itself onto the next sheet whole and leaves 40% of the paper
     empty, which is what the first printed proof looked like. Rows and
     fields are the units worth keeping intact; a card is not. */
  .card{
    background:#fff; border:1px solid var(--line); border-radius:0;
    box-shadow:none; margin:0 0 10px;
  }
  .card-head{
    background:#f1f3f5; border-bottom:1px solid var(--line); color:#111;
    /* without this the browser drops the fill and the heading loses its band */
    -webkit-print-color-adjust:exact; print-color-adjust:exact;
  }

  /* On screen these scroll sideways. On paper there is no sideways, and an
     overflow container clips instead of wrapping, which silently cuts the
     right-hand columns off a line-item table. */
  .table-scroll{overflow:visible}
  table{width:100%; border-collapse:collapse}
  th, td{border-bottom:1px solid var(--line); color:#111}
  thead{display:table-header-group}
  tr{break-inside:avoid; page-break-inside:avoid}

  .pill{border:1px solid var(--line); background:transparent; color:#111}
  a{color:inherit; text-decoration:none}

  /* One line, once, at the foot of the last sheet, hard right. In the document
     flow rather than position:fixed, and that is not a preference: Chrome does
     not paint a fixed element into a printed page at all, so the first version
     of this shipped a mark that never once reached paper. Proved by printing
     the page to PDF and searching it: the word Tallystick was not in the file. */
  .print-brand{
    display:block; margin:12px 0 0; text-align:right;
    /* One-off: the brand line on paper sits between --muted (#555 in
       print) and the rule, and reads as ink at #555. */
    font-size:8pt; color:#666; letter-spacing:.02em;
  }

  /* Paid takes the line off. Nothing else about the sheet changes, so what Pro
     removes is the advertising and not a feature of the document. */
  :root[data-pro="1"] .print-brand{display:none}

  /* ---- the printed page IS the downloaded PDF -------------------------

     CONTRACT.md section 1 already says the reader's preview is literally the
     PDF, and test_reader_mirror.mjs pins it: both are drawn from one op array
     out of pdfLayoutOps. Printing was the one place that promise broke, and
     it broke on scale rather than on content. app.js now repaints the sheet
     at 793.7px for print, which is 210mm at 96dpi, so one PDF point is one
     point of paper.

     The margin therefore comes from the DOCUMENT and not from the printer:
     the ops carry PDF_MG themselves, and a page margin on top of that would
     inset the sheet twice and shrink it away from the size the download has.
     Zero here is the whole point, and it is safe to state for this sheet
     because every page that loads app.css prints the reader's sheet. */
/* The sheet fills the page and the mark sits at its foot. min-height in vh,
     not a 297mm page: in print vh is the PRINTABLE height, so this survives a
     dialog that adds its own margin instead of spilling a second, near empty
     sheet. The bottom inset is the footer's own offset out of the layout,
     about 20mm, so the line is at the foot of the paper without being on the
     edge of it. */
  /* The sheet FILLS the printed page and the mark sits at its foot. Owner
     correction 2026-09-13 10:06: on paper the provenance row goes to the
     bottom edge of the sheet, the same place the downloaded PDF puts it;
     the preview on screen stays content-sized and is not touched by this
     rule. The engine hands the sheet flex column with .ops-gap as
     flex:1 1 auto between the body and .ops-foot, so stretching the sheet
     is all the pushing the footer needs. One px comes off the vh because a
     block exactly one printable page tall can round onto a second sheet,
     which is the "Total: 2 sheets" failure of 2026-09-10. The bottom inset
     is the layout's own paper inset (--pad-paper, the distance from the
     footer baseline to the A4 edge, about 20mm), so the printed sheet and
     the download agree on where the line sits; 16mm stays as the fallback
     for a sheet the engine did not stamp. */
  .ops-page{min-height:calc(100vh - 1px); padding-bottom:var(--pad-paper, 16mm)}

  @page{margin:0}

  /* If the ops layout ever declines, the HTML sheet prints instead, and that
     one has no margin of its own. */
  .invoice-render:not(.pdf-mode){padding:14mm}

  /* Invoice only, which is what the Download PDF button writes. The facts
     column and the cards under the sheet restate what the sheet already says
     and used to follow it onto a second sheet of paper. */
  /* #results.on > .facts-col carries an id, so the class on its own lost in
     silence and the facts column printed as a second sheet. Same ancestors,
     same weight, and this block is last. */
  #results.on > .facts-col,
  #results > .card, .render-head, .startover{display:none}

  /* The 4mm safety gutter above is for pages whose content has no margin of
     its own. This sheet has PDF_MG baked into every op, so the gutter is 4mm
     the printer takes OFF the document: painted at 794px inside a 764px box,
     the sheet lost its right-hand column, which is exactly the clipping this
     block set out to fix. The document owns its page. */
  .sheet-mode .wrap, .sheet-mode .wrap.tool{padding:0}

  /* Everything between the page box and the sheet, measured on / at A4:
     #results contributed 32px of padding on each side and a 1px border, and
     .invoice-render another 1px, so a document painted for 794px was laid
     into 726px and lost its right-hand column by exactly that much. On screen
     those are the card the sheet sits in; on paper the sheet IS the card.
     display:block as well, because the two-column grid keeps its 320px facts
     track even with the column hidden. */
  /* Written at the weight of the rules being overridden, not at the weight
     that reads best: the home card is body[data-tool="home"] #results.on
     (1,2,0) and the sheet's border is #results.on > #invoiceRender (2,1,0).
     A plain #results.on is (1,1,0) and lost to both in silence, which is the
     same failure that put this whole block at the end of the file. */
  body[data-tool="home"] #results.on,
  #results.on{display:block; padding:0; border:0; background:none; gap:0; margin:0}
  #results.on > #invoiceRender,
  #results.on > .xrc-host,
  .invoice-render{border:0; background:none; box-shadow:none; border-radius:0}

  /* The editorial band under the tool is the site talking about itself. It
     printed as a second sheet under the invoice. */
  section.editorial, .editorial{display:none}

}

/* =========================================================================
   (d) HOME HERO MAP, the mock of 2026-09-18. A pale land silhouette behind
       the hero (world-map.svg used as a CSS mask, so the palette owns the
       colour), dashed amber routes with anchor dots, and floating standard
       labels. Everything here is decoration: the whole layer is aria-hidden
       and pointer-events none in the markup, the labels hide below 1240px
       where the composition cannot hold, and the layer itself is clipped by
       the hero so the oversized canvas can never scroll the page.
   ========================================================================= */
body[data-tool="home"] .hero{
  overflow:hidden;
  background:linear-gradient(180deg, #FFFFFF 0%, var(--v5-bg) 78%);
}
.map-bg{
  /* component-scoped token: the land tint, one consumer */
  --map-land:#E1E8F1;
  position:absolute; inset:0; z-index:0; pointer-events:none;
}
/* the mock keeps the headline on open water: a soft white pool behind the
   hero copy so the text sits on near-white wherever the map runs beneath it */
.map-bg::before{
  content:""; position:absolute; z-index:1; left:50%; top:56px;
  transform:translateX(-50%); width:66%; height:380px;
  background:radial-gradient(50% 50% at 50% 50%, rgba(255,255,255,.95) 34%, rgba(255,255,255,0) 72%);
}
.map-canvas{
  position:absolute; top:0; left:50%; transform:translateX(-50%);
  width:115%;
}
.map-land{
  width:100%; aspect-ratio:1000/397;
  background:var(--map-land);
  -webkit-mask:url("/world-map.svg") 0 0/100% 100% no-repeat;
          mask:url("/world-map.svg") 0 0/100% 100% no-repeat;
}
/* the land dissolves into the page instead of stopping at a hard line */
.map-bg::after{
  content:""; position:absolute; inset:0;
  background:linear-gradient(180deg, transparent 66%, var(--v5-bg) 96%);
}
/* the thirteen format chips scattered over the land (generated by
   build.py from the same palette as the /convert-invoice wall): a
   white card, the coloured document mark, the format name - smaller
   than the wall chips because they are decoration over the map.
   Positions are canvas percentages, laid out so no two chips overlap
   and none crosses the headline at any width. */
.map-chip{
  position:absolute; display:inline-flex; align-items:center; gap:6px;
  background:var(--v5-surface); border:1px solid var(--v5-line);
  border-radius:8px; padding:4px 8px;
  box-shadow:0 1px 2px rgba(23,33,43,.05), 0 4px 10px rgba(23,33,43,.06);
  font-size:11.5px; font-weight:700; letter-spacing:-.01em; color:var(--v5-ink);
  white-space:nowrap;
}
.map-chip svg{width:18px; height:auto; flex:0 0 auto}
@media (max-width:1240px){ .map-chip{display:none} }
@media (max-width:680px){
  .map-canvas{width:220%}
  .dz-sil{display:none}
  /* the feature row stacks one per line here: compact tiles and copy so the
     stack reads as a tidy list, not three large cards */
  .feat-row{gap:10px 22px; margin-top:16px}
  .feat-ico{width:34px; height:34px; flex:0 0 34px; border-radius:9px}
  .feat-ico svg{width:16px; height:16px; max-width:16px; max-height:16px}
  .feat-copy b{font-size:14px}
  .feat-copy small{font-size:12px}
  body[data-tool="home"] .hero-sub{font-size:17px; margin:12px auto 26px}
}

/* the drop card: the dashed amber window is the site-wide .drop frame and
   the plate is the site-wide .tool-card (owner 2026-09-25, both values are
   home's everywhere), so only the home stage width and content sizes stay
   home-specific here; the invoice silhouettes are painted between the
   plate and the content */
body[data-tool="home"] .tool-stage{max-width:980px; margin-top:28px}
body[data-tool="home"] .drop-title{
  font-size:26px; font-weight:600; letter-spacing:-.015em;
}
body[data-tool="home"] .dz-orb{color:var(--v5-amber)}
body[data-tool="home"] .dz-orb svg{width:38px; height:38px}
.dz-sil{
  position:absolute; z-index:0; color:#5A6B80; opacity:.07; pointer-events:none;
}
.dz-sil-a{left:8px; top:18px; width:285px}
.dz-sil-b{right:8px; top:30px; width:295px}
body[data-tool="home"] .dz-head,
body[data-tool="home"] .drop-actions,
body[data-tool="home"] .export-row{position:relative; z-index:1}
body[data-tool="home"] .dz-head{gap:10px}
body[data-tool="home"] .drop-actions{margin-top:12px}
body[data-tool="home"] .drop .btn{margin-top:0}

@media print{
  /* Sanctioned exception to the PAPER note above: these render and sheet
     rules postdate that note and stay at the foot of the file. If this
     block grows, fold it into the main print block, never append below. */
  .render-toolbar, .notice, .render-head{display:none}

  /* A print viewport is about 794px, which falls inside the max-width:900px
     rule where .render-area pulls itself 22px wider than its container on
     each side. That is a full-bleed trick for a phone screen, and a phone
     screen has no edge to fall off; paper does. It put the sheet's right edge
     at 801px on a 794px page, so the last 7px of every line came off the
     thirteen to-PDF pages. Found 2026-09-10 by measuring the print layout at
     paper width instead of at a desktop width, which is the check the earlier
     pass did not make. */
  .render-area{margin-left:0; margin-right:0}
  /* In sheet mode the sheet carries the mark in its own footer, so the
     page-level .print-brand paragraph would print it a SECOND time. Measured
     on /xrechnung-to-pdf: two "Generated by tally-stick.com" lines on one free
     sheet, one under the totals and one under the page. */
  .sheet-mode .print-brand{display:none !important}
  .invoice-render{
    box-shadow:none; border-radius:0; padding:0; max-width:none;
    /* A print viewport is roughly 816px at 96dpi, which falls inside the
       narrow-screen rule above, and there the sheet scrolls sideways. An
       overflow container clips on paper instead of wrapping, which would cut
       the right-hand columns off the line-item table. */
    overflow:visible;
  }
  .invoice-render table{min-width:0}
}
