/* Plain, legible, and printable. The Monday entry pass happens on a desktop in
   the office; the read views get printed and carried into a Monday meeting. */
/* TYPE SCALE. One ratio, one place, so sizes relate to each other instead of
   being guessed per element.

   Eric asked for "minimum font size 14pt, 18pt is normal text". I first set the
   root to 18 POINTS, which is 24px, and then scaled everything else around that
   floor: body copy came out enormous, headings ended up barely larger than the
   paragraphs under them, and a Mass name in an entry row was nearly the size of
   the card's own title. His words for it: "you're married to some of those and
   scaled others based on my minimums ... some of these are too small and others
   are WAY TOO BIG."

   So: 18px is normal text and 14px is the floor, and everything between and
   above steps by roughly 1.2. Nothing here is set in a component; components use
   these tokens. --step-0 is body. Down is meta and labels, up is headings and
   figures. */
:root {
  --step--2: 0.78rem;   /* 14px, the floor: tile labels, chart ticks */
  --step--1: 0.87rem;   /* 15.5px: notes, captions, secondary lines */
  --step-0:  1rem;      /* 18px: body copy */
  --step-1:  1.25rem;   /* 22.5px: card titles */
  --step-2:  1.45rem;   /* 26px: page titles */
  --step-3:  2rem;      /* 36px: a figure that carries a card */
  --step-4:  2.2rem;    /* 40px: the one number a card is about */
}
html { font-size: 18px; }

:root {
  /* St. Edward parish palette. Every value below is measured, not eyeballed;
     the ratios are against parchment unless stated.

     Two of the parish colours cannot carry meaning and are chrome only:
     golden-sand is 1.50:1 and golden-bronze 2.02:1 against parchment, and the
     two sit only 8.5 apart in perceptual distance, under the 15 floor, so even
     full colour vision struggles to tell them apart. They rule, they divide,
     they never say anything. */
  --parchment:   #f4f1ea;   /* page ground */
  --ink:         #14202e;   /* body text, 14.59:1 */
  --navy:        #1f2c3f;   /* headings and the masthead, 12.49:1 */
  --emerald:     #004f2f;   /* actions and links, 8.62:1; white on it is 9.72:1 */
  --gold:        #c8a85a;   /* decorative rules only, never text */
  --gold-sand:   #d8c77a;   /* decorative only */

  --muted:       #556170;   /* secondary text, 5.59:1 */
  --line:        #ddd6c6;   /* card and table rules, warm to match parchment */
  --field:       #8a8578;   /* control borders: 3.68:1 on white, 3.26:1 on parchment */
  --ok:          #004f2f;   /* the emerald doubles as success */
  --warn:        #8c4a10;   /* 6.00:1 */
  --danger:      #96261c;

  /* Kept for the rules that still say --paper and --accent. */
  --paper:       #f4f1ea;
  --accent:      #004f2f;
}

* { box-sizing: border-box; }
body {
  margin: 0;
  /* The size comes from the scale, not from this shorthand. A hardcoded 16px
     here quietly overrode the root, so every rem in the file was measured
     against one number while the scale was built around another, and the whole
     hierarchy stayed squashed no matter what the steps said. */
  font: var(--step-0)/1.55 -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
  color: var(--ink);
  background: var(--paper);
}
/* main sits beside the sidebar now, so its own centring is gone: the shell
   does the layout and main takes what is left. The 80vw it used to claim was
   measured against the whole window and would put the content off-centre
   inside a narrower column. */
main {
  flex: 1 1 auto;
  min-width: 0;
  padding: 1.75rem 2rem 4rem;
  max-width: 82rem;
}

/* ==========================================================================
   The shell: a sidebar and a page, matching the Service System.
   ==========================================================================

   Eric, 2026-08-21, holding ticket.stedward.org next to this one: "this is the
   clean, organized, way I like seeing data." What the two apps share now is
   where things ARE. Somebody moving between the parish's own systems should not
   have to find the navigation twice.

   The navigation left rather than across the top is the whole of it. A top bar
   spends the widest part of the screen on five links and pushes the first
   number below the fold; a column spends the narrowest part on them and gives
   the page the rest.
*/
.skiplink {
  position: absolute; left: -9999px; top: 0; z-index: 10;
  background: var(--navy); color: #fff; padding: 0.6rem 0.9rem;
  border-radius: 0 0 4px 0; text-decoration: none; font-weight: 600;
}
.skiplink:focus { left: 0; }

.shell { display: flex; align-items: flex-start; min-height: 100vh; }

/* STICKY and one viewport tall, not as tall as the page. Stretched to the
   shell it grew to the full document height, so "Other apps" and the account
   block sat thousands of pixels below the fold and the navigation scrolled away
   from anybody reading the bottom of a long dashboard. */
.side {
  flex: 0 0 15rem; width: 15rem;
  position: sticky; top: 0;
  height: 100vh; overflow-y: auto;
  background: #fff;
  border-right: 1px solid var(--line);
  padding: 1.25rem 0.9rem 1rem;
  display: flex; flex-direction: column; gap: 1.25rem;
}
.brand { display: block; text-decoration: none; padding: 0 0.5rem; }
.brand-org {
  display: block; font-weight: 700; line-height: 1.2;
  color: var(--navy); font-size: var(--step-0);
}
.brand-app {
  display: block; margin-top: 0.2rem;
  font-size: var(--step--2); font-weight: 600;
  letter-spacing: 0.08em; text-transform: uppercase; color: var(--muted);
}

.side nav { display: flex; flex-direction: column; gap: 0.15rem; }
.side nav a {
  display: block; padding: 0.5rem 0.6rem; border-radius: 5px;
  color: var(--ink); text-decoration: none; font-size: var(--step--1);
}
.side nav a:hover { background: var(--parchment); }
.side nav a[aria-current="page"] {
  background: var(--parchment); font-weight: 600; color: var(--navy);
  box-shadow: inset 3px 0 0 var(--emerald);
}

.side-group { margin-top: auto; }
.side-label {
  margin: 0 0 0.35rem; padding: 0 0.6rem;
  font-size: var(--step--2); font-weight: 600;
  letter-spacing: 0.08em; text-transform: uppercase; color: var(--muted);
}
.side-apps { list-style: none; margin: 0; padding: 0; }
.side-apps a {
  display: block; padding: 0.3rem 0.6rem;
  font-size: var(--step--2); color: var(--muted); text-decoration: none;
}
.side-apps a:hover { color: var(--emerald); text-decoration: underline; }

.side-foot {
  display: flex; align-items: center; gap: 0.55rem; flex-wrap: wrap;
  border-top: 1px solid var(--line); padding: 0.85rem 0.6rem 0;
}
.side-avatar {
  flex: 0 0 auto; width: 2rem; height: 2rem; border-radius: 50%;
  background: var(--navy); color: #fff;
  display: grid; place-items: center;
  font-size: var(--step--2); font-weight: 700;
}
.side-who { display: flex; flex-direction: column; line-height: 1.2; min-width: 0; }
.side-who strong { font-size: var(--step--1); color: var(--navy); }
.side-who .muted { font-size: var(--step--2); }
.side-out { font-size: var(--step--2); width: 100%; margin-top: 0.15rem; }

/* Under 900px the column becomes a strip across the top, because a 15rem
   sidebar on a 390px phone is most of the phone. */
@media (max-width: 900px) {
  .shell { display: block; }
  .side {
    width: auto; flex: none;
    position: static; height: auto; overflow: visible;
    flex-direction: row; flex-wrap: wrap; align-items: center;
    gap: 0.5rem 1rem;
    border-right: 0; border-bottom: 1px solid var(--line);
    padding: 0.8rem 1rem;
  }
  .brand { padding: 0; }
  .brand-org { font-size: var(--step--1); }
  .brand-org br { display: none; }
  .side nav { flex-direction: row; flex-wrap: wrap; gap: 0.15rem 0.4rem; }
  .side nav a[aria-current="page"] { box-shadow: inset 0 -2px 0 var(--emerald); }
  .side-group { margin-top: 0; width: 100%; }
  .side-apps { display: flex; flex-wrap: wrap; gap: 0 0.9rem; }
  .side-foot { width: 100%; border-top: 0; padding-top: 0; }
  .side-out { width: auto; margin-top: 0; margin-left: auto; }
  main { padding: 1.25rem 1rem 3rem; }
}

/* THE CRUMB TRAIL above an entry screen's heading. Orientation, not
   navigation. The sidebar is where somebody GOES and it is already the loudest
   thing on the left of the page; this only says where they already are, so it
   is a step down from body, muted, and pressed close under the heading it
   belongs to rather than floating as a band of its own. Nothing above it: it is
   the first thing inside main and main already brings its own top padding, and
   a margin there would push the page's title down for no one's benefit.

   It wraps rather than overflows, because the last crumb is a metric label or a
   Mass name and those run long: "Offertory in LGL, cash and check" is wider
   than a 390px phone on its own. */
.crumbs {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 0.1rem 0.4rem;
  margin: 0 0 0.6rem;
  font-size: var(--step--1);
  color: var(--muted);
}
/* The page's own link colour, so a crumb behaves like every other link in the
   app and nobody has to learn a second one. */
.crumbs a { color: var(--emerald); }
/* The separator says nothing, which is why the markup hides it from a screen
   reader, so it is painted lighter than the words on either side of it.
   --field is the lightest value in the palette that is still a mark rather than
   a ghost: 3.26:1 on parchment, against the muted text's 5.59:1. */
.crumbs [aria-hidden="true"] { color: var(--field); }
/* The page you are already on is not a place you can go, so it must not look
   like one. Muted, the same weight as the trail around it, and deliberately not
   the link colour. */
.crumbs [aria-current="page"] { color: var(--muted); }

h1 { font-size: var(--step-2); margin: 0 0 0.25rem; color: var(--navy); }
h2 { font-size: var(--step-1); margin: 0 0 0.5rem; color: var(--navy); }
.lede { color: var(--muted); font-size: var(--step--1); margin: 0 0 1.5rem;
        line-height: 1.5; }
.help { background: #fff; border-left: 3px solid var(--gold); padding: 0.6rem 0.8rem; color: var(--muted); }
.muted { color: var(--muted); font-size: 0.87em; }

.card { background: #fff; border: 1px solid var(--line); border-radius: 6px; padding: 1rem; margin-bottom: 1rem; }
.total { font-size: var(--step-1); font-weight: 600; margin: 0.3rem 0; }
.ok { color: var(--ok); }
.warn { color: var(--warn); }
.warn-inline { color: var(--warn); font-size: 0.85em; }

table { border-collapse: collapse; width: 100%; margin: 0.5rem 0; }
th, td { text-align: left; padding: 0.45rem 0.5rem; border-bottom: 1px solid var(--line); vertical-align: top; }
th { font-size: var(--step--1); text-transform: uppercase; letter-spacing: 0.03em; color: var(--muted); }
tr.superseded td, tr.voided td { color: var(--muted); text-decoration: line-through; }
a { color: var(--emerald); }
a:hover { color: var(--navy); }

/* A LINK THAT CARRIES ITS OWN BACKGROUND MUST SET ITS OWN HOVER COLOUR.
   The rule above turns any link navy on hover, which is correct on parchment and
   invisible everywhere else. It hits the three .button links, including
   "Continue to Microsoft" on the sign-in page, which is the first control every
   person who has never signed in touches: navy label on an emerald button.
   Class beats type, so these win over a:hover without needing :not().

   The brand USED to need the same treatment in the opposite direction, white on
   the navy masthead. The masthead is gone as of 2026-08-21 and the brand sits on
   a white sidebar, so that rule became white on white: 1.00:1, caught by
   scripts/check_contrast.py on the first run after the change and not by any
   test. Its colour is carried by the two spans inside it now, which have their
   own, so it sets none of its own and inherits nothing harmful. */
.brand:hover .brand-org, .brand:focus-visible .brand-org { color: var(--emerald); }
.brand:hover .brand-app, .brand:focus-visible .brand-app { color: var(--emerald); }
a.button:hover, a.button:focus-visible { color: #fff; background: #003a22; }

input, select, textarea {
  font: inherit; padding: 0.4rem 0.5rem; border: 1px solid var(--field);
  border-radius: 4px; background: #fff; max-width: 100%;
}
fieldset { border-color: var(--field); }
input[type="number"], input[inputmode="numeric"], input[inputmode="decimal"] { width: 7rem; }

/* There was no focus style anywhere in this file, so every focus indicator was
   the browser default, which differs across the office desktop, the meeting
   laptop and a phone, and has almost nothing to sit against on a maroon button
   (ink on the accent is 1.48:1). :where() keeps specificity at zero so nothing
   above has to be reordered. */
:where(a, button, .button, input, select, textarea):focus-visible {
  outline: 3px solid var(--accent);
  outline-offset: 2px;
}
button:focus-visible, .button:focus-visible {
  outline-color: var(--ink);
  box-shadow: 0 0 0 2px #fff;
}
/* Which row you are on, working down five Masses by keyboard. */
table.entry tr:focus-within td, table.entry tr:focus-within th { background: #faf7ee; }

/* Touch only. A global 44px would add about 60px to the weekend grid and grow
   the inline correction form on the office desktop, where compactness is what
   lets the typist see the whole sheet at once. */
@media (pointer: coarse) {
  input, select, textarea, button, .button { min-height: 44px; }
}
label { display: block; margin-bottom: 0.9rem; }
label.check { display: flex; gap: 0.5rem; align-items: center; }
fieldset { border: 1px solid var(--line); border-radius: 4px; margin-bottom: 0.9rem; }
.stack { max-width: 28rem; }

button, .button {
  font: inherit; display: inline-block; padding: 0.5rem 0.9rem;
  background: var(--emerald); color: #fff; border: 0; border-radius: 4px;
  cursor: pointer; text-decoration: none;
}
form.inline { display: flex; gap: 0.3rem; align-items: center; }
form.inline input[type="text"] { width: 9rem; }
.person { background: #fff; border: 1px solid var(--line); border-radius: 6px; padding: 1rem; margin-bottom: 1rem; }
.recorded { font-weight: 600; }

.flashes { list-style: none; padding: 0; margin: 0 0 1rem; }
.flash { padding: 0.6rem 0.8rem; border-radius: 4px; margin-bottom: 0.4rem; background: #fff; border-left: 4px solid var(--muted); }
.flash.ok { border-left-color: var(--ok); }
.flash.error { border-left-color: var(--danger); }
/* Saved, but worth a look. Distinct from error, which means not saved. */
.flash.note { border-left-color: var(--warn); }

.nav-weekends { display: flex; gap: 1rem; margin: 0 0 1rem; }
.metric-list { list-style: none; padding: 0; }
.metric-list li { padding: 0.3rem 0; }

footer { border-top: 1px solid var(--line); padding: 1rem; text-align: center; }
footer p { color: var(--muted); font-size: var(--step--1); margin: 0; }

/* The weekend count grid, shaped like the paper sheet. */
/* th.mass AS WELL AS td.mass, which is the whole of why this screen did not fit
   a phone. The row header is a <th scope="row" class="mass"> for the screen
   reader, so neither of these rules had ever matched it: the date span stayed
   inline and nowrap kept it on one line, making "Saturday 5:00 PM Vigil Sat 15
   Aug" a single 298px cell inside a 390px window. The rule was written to put
   the date on its own line and had simply never run. */
table.entry td.mass, table.entry th.mass { white-space: nowrap; }
table.entry td.mass span, table.entry th.mass span { display: block; }
table.entry tr.totals td, table.entry tr.totals th { border-top: 2px solid var(--navy); background: #fff; }
label.block { max-width: 40rem; }
label.block textarea { width: 100%; }

/* Single-entry forms: label above its field, every field the same shape. */
.stack label { display: block; margin-bottom: 1.1rem; font-weight: 600; }
.stack label input,
.stack label select,
.stack label textarea {
  display: block;
  margin-top: 0.3rem;
  font-weight: 400;
}
/* min-width is applied after max-width and wins, so 24rem overflowed a 375px
   phone sideways. That select is the confession session picker, the one screen
   the two priests ever touch. .stack is already capped at 28rem, so desktop is
   unchanged. */
.stack select { width: 100%; max-width: 100%; }
.stack textarea { width: 100%; }
.stack input[type="date"] { width: 12rem; }

form.periodpick { margin-bottom: 1rem; }
form.periodpick label { display: inline-flex; gap: 0.5rem; align-items: center; margin: 0; }

/* Money and counts align on their digits. A jittering column is what hides a
   transposed digit from somebody checking their typing against paper. Works on
   the system stack with no font added. */
.recorded, .total, .tile-value, .barvalue,
table.entry td, table.entry th[scope="row"],
.entries td, .chart .barvalue { font-variant-numeric: tabular-nums; }

/* Cap the prose, not the container: these sentences are the whole instruction
   layer for four people who will have forgotten the interface. footer p is
   centred and would become a left-anchored block, so it is excluded. */
main > p, .card > p, .lede, .help, .muted { max-width: 62ch; }
footer p { max-width: none; }

/* An empty figure is a different shape, not a dimmer one. Dimness disappears
   across a meeting table, and a greyed number is still a number once somebody
   reads it aloud. */
.tile-empty {
  display: block; font-size: var(--step-0); font-weight: 400; line-height: 1.2;
  color: var(--muted); font-style: italic; padding: 0.35rem 0;
}
.no-total {
  display: block; font-size: var(--step--1); font-weight: 400; color: var(--warn);
}

/* ---------- Dashboard ---------- */
.sample {
  background: #fdf6e3; border-left: 4px solid var(--warn);
  padding: 0.6rem 0.8rem; border-radius: 4px; color: var(--warn);
}
/* Was 0.95rem uppercase muted, so a chart's own title rendered smaller and
   lighter than the caption beneath it. */
h3 { font-size: var(--step-0); margin: 1.5rem 0 0.5rem; color: var(--navy); font-weight: 600; }

.tiles { display: flex; flex-wrap: wrap; gap: 1.5rem; margin: 0.5rem 0 0.5rem; }
.tile { min-width: 10rem; }
.tile-label { display: block; font-size: var(--step--2); color: var(--muted); }
.tile-value { display: block; font-size: var(--step-2); font-weight: 600; line-height: 1.2; }
.tile-note { display: block; font-size: var(--step--2); color: var(--muted); }
/* Direction is stated in the text (+/-), never by colour alone. */
.tile-value.up { color: var(--emerald); }
.tile-value.down { color: var(--danger); }

/* A real number built from a set that is not whole yet. It stays full tile
   size and full colour on purpose: shrinking or greying it would say this
   figure matters less, and it does not, it is exactly as read-aloud-real as
   any other tile, just not yet finished. The signal is a shape instead of a
   colour, a dashed underline in the same language .recon-incomplete already
   uses for "not the whole story," so it survives a black-and-white photocopy
   carried into the Monday meeting the way a greyed number would not. */
.tile-value.partial {
  border-bottom: 3px dashed var(--field);
  padding-bottom: 0.05em;
}

/* The same fact as the number it sits beside, never a footnote about it: one
   step down from body and never italic, because italic muted is the language
   .tile-empty and .chart .missing already spend on "nothing here yet," and
   this is the opposite of nothing, it is what was actually written down. It
   appears inside a tile's .tile-note, inside a chart's own <h3>, and in plain
   body copy, so the size is pinned to a token instead of being left to
   inherit three different parent sizes in those three places. */
.coverage {
  font-size: var(--step--1);
  font-weight: 400;
  font-style: normal;
  color: var(--muted);
}

.chart { width: 100%; height: auto; display: block; overflow: visible; }
.chart .grid { stroke: #e2ddce; stroke-width: 1; }
.chart .axis { stroke: #c4bda8; stroke-width: 1; }
.chart .tick { fill: var(--muted); font-size: 13px; }
.chart .pointlabel { fill: var(--ink); font-size: 13px; font-weight: 600; }
.chart .barlabel { fill: var(--ink); font-size: 14px; }
.chart .barvalue { fill: var(--ink); font-size: 14px; font-weight: 600; }
.chart .missing { fill: var(--muted); font-size: 14px; font-style: italic; }

/* What was entered for a period the line will not draw a real point for yet.
   fill: none instead of the series colour lets whatever the circle sits over
   show through it, so it reads as "there is something here and it is not one
   of the real points" at a glance, which is exactly what the template's own
   comment on this circle says it needs to do. Same r="4.5" as the solid
   points, set in the markup rather than here, so size never has to carry the
   difference either; only the hollow fill and this stroke width do. */
.chart .partial {
  fill: none;
  stroke-width: 2;
}
.chart.bars { max-width: 100%; }

/* The average, drawn across the plot. One shade darker than the grid so it
   reads as a statement rather than as another gridline, and SOLID: a dashed
   rule on a chart reads as a projection or a threshold, and this is neither.
   It is named in a legend above the chart rather than labelled inside it,
   because the average sits by definition near the data and a label at the
   right-hand end printed straight through the last point's own value. */
.chart .avgrule { stroke: #9a9482; stroke-width: 1.5; }
.ruleswatch { width: 18px; height: 0; border-top: 1.5px solid #9a9482;
              display: inline-block; vertical-align: middle; }
.legenditem { display: inline-flex; align-items: center; gap: 0.4rem;
              font-size: var(--step--1); color: var(--muted); }

/* The ring, and the key that carries its numbers. The key is not decoration:
   a colour with no name beside it is identity by colour alone, which nobody
   reading in greyscale or printing this page can use, and the values in it are
   how the chart is read without hovering anything. */
.donutwrap { display: flex; flex-wrap: wrap; align-items: center;
             gap: 1.5rem 2rem; margin: 0.4rem 0 0.6rem; }
.chart.donut { width: auto; max-height: 15rem; flex: 0 0 auto; }
.chart .slicelabel { fill: var(--ink); font-size: 13px; font-weight: 600; }
.chart .donuttotal { fill: var(--ink); font-size: 26px; font-weight: 700; }
.chart .donutfoot { fill: var(--muted); font-size: 13px; }
.donutkey { list-style: none; margin: 0; padding: 0; flex: 1 1 16rem;
            min-width: 15rem; max-width: 26rem; }
.donutkey li { display: grid; grid-template-columns: 12px 1fr auto auto;
               align-items: center; gap: 0 0.6rem;
               padding: 0.28rem 0; border-bottom: 1px solid var(--line);
               font-size: var(--step--1); }
.donutkey li:last-child { border-bottom: 0; }
.donutkey .keyname { color: var(--ink); }
.donutkey .keyvalue { color: var(--ink); font-weight: 600;
                      font-variant-numeric: tabular-nums; }
.donutkey .keyshare { color: var(--muted); min-width: 2.6rem; text-align: right;
                      font-variant-numeric: tabular-nums; }

.legend { display: flex; gap: 1.2rem; margin: 0 0 0.3rem; }
.key { display: inline-flex; align-items: center; gap: 0.4rem;
       font-size: var(--step--1); color: var(--muted); }
.swatch { width: 12px; height: 12px; border-radius: 3px; display: inline-block; }

/* One print block. The previous pair hid every `form` with an unqualified
   selector, and the weekend grid, the sacraments grid and every person block
   each live inside one, so printing those screens produced a heading and a
   blank page, in an app whose premise is that paper is the backup.
   Hide the controls and the chrome, never the container. */
@media print {
  @page { margin: 14mm; }

  .bar nav, .button, button,
  form.inline, form.periodpick,
  .flashes, .nav-weekends { display: none; }

  /* .crumbs is deliberately NOT in the list above. It is a nav element, and the
     rule that hides navigation is scoped to `.bar nav` for that reason. On
     paper the trail is the only thing naming which weekend or which metric the
     sheet in somebody's hand belongs to, and a sheet carried into a meeting
     with no name on it is the failure this app exists to prevent. */

  /* Entered values print; empty cells print empty. Deliberately NOT ruled
     underlines: that would be a second blank count sheet competing with the
     real paper form, and D10 records that the sheet as drawn is the sheet. */
  table.entry input, table.entry textarea, label.block { display: none; }

  body { background: #fff; }
  .card { border: 1px solid #999; padding: 0.6rem; break-inside: avoid; }
  .tiles { gap: 1rem; }
  h2, h3 { break-after: avoid; }

  /* Survive a photocopy. */
  th, td { border-bottom-color: #999; }
  .chart .grid { stroke: #bbb; }

  /* A sample-data banner must never print looking like ordinary content. */
  .sample { print-color-adjust: exact; -webkit-print-color-adjust: exact; }
}

/* Column identity on the entry grid. The two columns are structurally
   identical and tab order is row-major, so five attendance numbers typed
   tab-by-tab could land in the offertory column with nothing to catch it. */
table.entry td.count input { width: 4.5rem; }
table.entry td.money input { width: 7rem; }


table.entry .dollar { color: var(--muted); margin-right: 0.15rem; }
/* A half-filled sheet should not go ragged where an input became text. */
table.entry .recorded {
  display: inline-block; min-width: 4.5rem;
  padding: 0.4rem 0.5rem; border: 1px solid transparent;
}
table.entry th[scope="row"] {
  font-size: var(--step-0); text-transform: none; letter-spacing: 0;
  color: var(--ink); font-weight: 400; text-align: left; white-space: nowrap;
}
.visually-hidden {
  position: absolute; width: 1px; height: 1px; overflow: hidden;
  clip: rect(0 0 0 0); clip-path: inset(50%); white-space: nowrap;
}

/* Tabs. One subject at a time, because this is a collector before it is an
   integrator: attendance, giving and work orders come from different places and
   reading them stacked in one column tells you nothing their own page does not.
   Links, not JavaScript, so a tab is a URL somebody can send to somebody else. */
.tabs {
  display: flex; flex-wrap: wrap; gap: 0.25rem;
  margin: 0 0 1.25rem; border-bottom: 2px solid var(--line);
}
.tabs a {
  padding: 0.6rem 0.95rem; text-decoration: none; color: var(--muted);
  font-weight: 600; border-bottom: 3px solid transparent; margin-bottom: -2px;
  min-height: 44px; display: flex; align-items: center;
}
.tabs a:hover { color: var(--ink); }
.tabs a[aria-current="page"] { color: var(--navy); border-bottom-color: var(--emerald); }

/* Completeness sits above the tabs and is never inside one. A gap the reader
   has to click a tab to discover is a gap that gets read aloud as a real
   number. */
.gapstrip {
  margin: 0 0 1rem; padding: 0.7rem 0.9rem;
  border-left: 4px solid var(--warn); background: #fdf6ec;
  border-radius: 0 6px 6px 0; font-size: var(--step--1);
}
.gapstrip.ok { border-left-color: var(--ok); background: #eef4f0; }
.gapstrip a { color: var(--emerald); }

/* A quiet inline action, not a second filled button beside the weekend link:
   the banner already reads as urgent from its warm ground and its warn-
   coloured rule, and a solid emerald button here would be the loudest thing
   in it for the smallest decision on the page. Coloured and underlined like
   the weekend link it sits next to instead of styled like button/.button.
   The warm ground it has to sit on is .gapstrip's own #fdf6ec just above,
   lighter than parchment, so emerald's measured 8.62:1 against parchment only
   gets better against it. min-height is a belt-and-suspenders 24px floor;
   this class does not touch display, so the print rule that hides every other
   button on the page (`button { display: none; }`, above) still hides this
   one without a fight over specificity. */
/* The gaps list, and the actions that live on it. A weekend, what it is short,
   and the one button that settles it, on one line. The form wrapper has to be
   inline or the button drops to a line of its own: a form is a block element,
   and the first version of this strip printed every action underneath its
   weekend with the separators stranded on lines between them. */
.gaplist { list-style: none; margin: 0.35rem 0 0; padding: 0; }
.gaplist li {
  display: flex; flex-wrap: wrap; align-items: baseline;
  gap: 0.15rem 0.55rem; padding: 0.2rem 0;
}
.gapform { display: inline; margin: 0; }

/* The settled strip sits under the gaps banner and must not look like another
   alarm: these weekends are answered, not outstanding. Quiet ground, no
   coloured edge, and it only exists at all so that writing a weekend off is
   never a one-way door. */
.settledstrip {
  margin: 0 0 1.1rem; padding: 0.7rem 0.9rem;
  border-left: 3px solid var(--line); background: transparent;
}
.settledstrip p { margin: 0; color: var(--muted); font-size: var(--step--1); }

.gapaction {
  background: none;
  border: 0;
  padding: 0.15rem 0.35rem;
  min-height: 24px;
  color: var(--emerald);
  font: inherit;
  text-decoration: underline;
  cursor: pointer;
}
.gapaction:hover { color: var(--navy); }

/* The read views get a wider page than the entry forms. A 60rem column on a
   1440 screen left a third of the width empty and pushed the charts into a
   ribbon. */
body.wide main { width: 80vw; max-width: 80vw; }

@media (max-width: 40rem) {
  .tabs { gap: 0; }
  .tabs a { padding: 0.6rem 0.7rem; font-size: var(--step--1); }
}

/* On a phone an SVG that scales to the viewport takes its axis labels down to
   about four pixels, which is a chart nobody can read pretending to be one. Hold
   the chart at a legible width and let it scroll inside its own card instead.
   The page itself never scrolls sideways; only the chart does. */
@media (max-width: 46rem) {
  .card { overflow-x: auto; }
  /* The long line chart genuinely needs the room: 52 points in 356px is a
     smudge. The per-Mass bars do not, and holding them wide cut the value off
     mid-digit, so 354 rendered as "35". A truncated number that still looks
     like a number is the one thing this page must never do. */
  .chart { min-width: 32rem; }
  .chart.bars { min-width: 0; }
  .chart.donut { min-width: 0; max-height: 13rem; }
  .donutkey { min-width: 0; }
}

/* What the team is looking at, in a sentence. */
.blurb { margin: 0 0 1rem; color: var(--muted); max-width: 46rem; }

/* Where the numbers come from. Folded away by default because it is reference,
   opened automatically when something is out of date, because that is the one
   time the team needs to see it without being told to look. */
.status { margin: 0 0 1.25rem; border: 1px solid var(--line); border-radius: 8px;
          background: #fff; }
.status > summary {
  cursor: pointer; padding: 0.7rem 0.9rem; font-weight: 600; color: var(--navy);
  display: flex; align-items: center; gap: 0.6rem; min-height: 44px;
}
.status-flag {
  font-weight: 600; font-size: var(--step--2); color: #fff; background: var(--warn);
  padding: 0.1rem 0.5rem; border-radius: 999px;
}
.status-list { list-style: none; margin: 0; padding: 0 0.9rem 0.9rem; }
.status-list li {
  display: grid; gap: 0.15rem; padding: 0.7rem 0; border-top: 1px solid var(--line);
}
.status-list li.is-stale { border-left: 3px solid var(--warn); padding-left: 0.6rem; }
.status-what { font-weight: 600; }
.status-how {
  justify-self: start; font-size: var(--step--2); font-weight: 600; letter-spacing: 0.02em;
  text-transform: uppercase; padding: 0.1rem 0.45rem; border-radius: 4px;
  border: 1px solid var(--line); color: var(--muted);
}
.status-how.auto   { color: var(--emerald); border-color: var(--emerald); }
.status-how.manual { color: var(--navy);    border-color: var(--field); }
.status-how.sample { color: var(--warn);    border-color: var(--warn); }

/* A slot somebody has recorded as never going to be counted, not a slot
   nobody has gotten to yet. .tile-empty and .chart .missing are muted italic
   for "nothing here" and this needs to read as the opposite of that: a person
   looked, and decided. Deliberately not one of the ledger's pills either:
   those three colours are a good/caution/bad verdict on a number (active,
   superseded, voided), and a closed gap carries no verdict at all, so
   borrowing one of them would claim a judgement this status does not make.
   Square corners instead of a pill, navy instead of a status colour, the same
   shape .status-how.manual already uses just above for "a plain fact, no
   direction to it." */
.not-counted {
  display: inline-block;
  padding: 0.1rem 0.45rem;
  border: 1px solid var(--field);
  border-radius: 4px;
  color: var(--navy);
  font-weight: 600;
  font-size: var(--step--2);
  letter-spacing: 0.02em;
  text-transform: uppercase;
  font-style: normal;
  white-space: nowrap;
}
.status-when { font-size: var(--step--1); }
.status-detail { font-size: var(--step--1); color: var(--muted); }
/* Two classes, because one loses. `.warn` is declared near the top of this file
   and `.status-detail` after it, so a bare `.warn` on a status line was
   overruled by the muted colour and the sentence saying a source has gone quiet
   read exactly like the sentence saying it has not. */
.status-detail.warn { color: var(--warn); }

/* The hover readout, on anything inside a chart that answers for a number: the
   line charts' hit bands, a donut slice, a bar, a stacked segment. The cursor
   is the only thing on the page that says a shape is worth pointing at before
   it has been pointed at. */
.hit { cursor: pointer; }
.hit:focus { outline: none; }
.hit:focus-visible { outline: 2px solid var(--emerald); outline-offset: 2px; }
.readout {
  position: absolute; z-index: 20; pointer-events: none;
  background: var(--navy); color: #fff; padding: 0.35rem 0.6rem;
  border-radius: 6px; font-size: var(--step--1); white-space: nowrap;
  box-shadow: 0 2px 8px rgba(20, 32, 46, 0.25);
}

/* The reconciliation block. The disagreement is the product here, so a
   divergence is a different shape and not a redder version of the same row. */
.recon { list-style: none; margin: 0 0 1rem; padding: 0; display: grid; gap: 0.75rem; }
.recon li {
  display: grid; gap: 0.3rem; padding: 0.85rem 1rem;
  border: 1px solid var(--line); border-radius: 8px; background: var(--parchment);
}
.recon li.recon-agrees { border-left: 4px solid var(--ok); }
.recon li.recon-diverges { border-left: 4px solid var(--danger); background: #fdf1f0; }
.recon li.recon-incomplete { border-left: 4px dashed var(--field); }
.recon-label { font-weight: 700; color: var(--navy); }
.recon-pair { display: flex; align-items: baseline; gap: 0.9rem; flex-wrap: wrap; }
.recon-side { display: flex; flex-direction: column; }
.recon-cap {
  font-size: var(--step--2); text-transform: uppercase; letter-spacing: 0.04em;
  color: var(--muted); font-weight: 600;
}
.recon-val { font-size: var(--step-2); font-weight: 700; font-variant-numeric: tabular-nums; }
.recon-vs { color: var(--muted); font-size: var(--step--1); align-self: center; }
.recon-gap { font-size: var(--step--1); }
.recon-diverges .recon-gap strong { color: var(--danger); }
.recon-says { font-size: var(--step--1); }
.recon-diverges .recon-says { font-weight: 600; }
.recon-note { font-size: var(--step--1); color: var(--muted); }

/* Nothing drawn inside a chart steals the hover except the hit bands, and now
   the partial markers: a weekend with only a partial figure has no entry in
   chart.points, so it never gets a hit band drawn for it at all, and its own
   native <title> is the only readout it will ever have. The selective point
   labels and the axis ticks sit on top of a few bands, and where they did the
   readout showed nothing at all for that week.

   THE SECOND LINE IS WHAT MAKES A SHAPE ANSWERABLE, and the first line will
   silently kill any shape that does not carry it. The donut arcs are the
   cautionary tale: every ring on the dashboard was drawn as a path with a
   <title> written for it, the blanket `.chart path` rule above meant the arcs
   never received a pointer event, and so not one of those tooltips had fired
   since the day this rule was written. Nothing looked wrong in the markup and
   nothing failed in the suite; it was only visible to somebody hovering it.
   `.chart .hit` outranks `.chart path` on specificity, so a slice opts back in
   by carrying the hit class the readout keys on anyway, and one class both
   restores the pointer and wires up the readout box.

   So: anything inside a chart that is meant to answer a person needs the .hit
   class, data-when, data-value, tabindex and an aria-label, together. Half of
   that set is a shape that looks interactive and says nothing. */
.chart text, .chart .grid, .chart .axis, .chart path, .chart circle { pointer-events: none; }
.chart .hit, .chart .partial { pointer-events: all; }

/* The stall tile. Not a redder version of the other tiles: this is the one
   figure the project was started over, and it earns a shape of its own. */
.tile.alarm {
  border-left: 4px solid var(--danger); padding-left: 0.7rem;
  background: #fdf1f0; border-radius: 0 6px 6px 0;
}
.tile.alarm .tile-value { color: var(--danger); }

/* ==========================================================================
   The dashboard's own layout. Parchment, like the rest of the app.
   ==========================================================================

   Eric, 2026-08-20, holding four real dashboards next to this page: "those are
   what dashboards look like. your dashboard looks like [craigslist]." The thing
   worth taking from those references was never the dark ground, which he said
   plainly afterwards. It was the shape: one number per box at a size you can
   read across a room, the change against last period stated beside it rather
   than left to be worked out, and the reading starting immediately instead of
   after a paragraph explaining it.

   So this is that shape on the parish's own parchment. Scoped to body.dash, so
   the entry forms keep their quieter treatment.
*/
body.dash .card { padding: 1.1rem 1.2rem 1.3rem; }
body.dash .card h2 {
  font-size: var(--step-1);
  letter-spacing: -0.01em;
  margin-bottom: 0.8rem;
}
body.dash .card h3 {
  font-size: var(--step--2);
  text-transform: uppercase;
  letter-spacing: 0.09em;
  color: var(--muted);
  margin: 1.5rem 0 0.6rem;
}

/* One number per box, and the box is visible. A tile row was borderless text at
   1.7rem, which reads as a paragraph with some bold in it. */
/* Flex, not grid, and every tile may grow. A grid track count is fixed by the
   container width, so five tiles in a four wide row left the fifth stranded on a
   line of its own beside an empty half of the card. Flex items stretch to fill
   the row they land on, so a row is always a row. */
body.dash .tiles {
  display: flex;
  flex-wrap: wrap;
  gap: 0.85rem;
  margin: 0 0 0.4rem;
}
/* 7.5rem, not 11.5rem. At 18pt root a 11.5rem basis is 276px, so four tiles
   wanted 1,100px inside a card about 780px wide: three sat on the first row and
   the fourth stretched across the second on its own, which is the stranded box
   again in a different shape. Flex still grows them to fill whatever row they
   land on. */
/* Wide enough for a money figure at the size above. Four across on a full
   screen, and they still grow to fill whatever row they land on. */
body.dash .tile { flex: 1 1 10rem; }
/* WHITE on the parchment ground with a coloured edge, rather than parchment on
   parchment. Eric, 2026-08-21, pointing at the Service System's counters: the
   tile reads as an object sitting on the page instead of a lighter patch of it,
   and the edge says what KIND of number it is before the number is read.
   The colour is meaning, never decoration: emerald for a figure where rising is
   good, amber for one that wants a look, red for one that wants an answer
   today, navy for a plain count with no direction to it. */
body.dash .tile {
  background: #fff;
  border: 1px solid var(--line);
  border-left: 4px solid var(--navy);
  border-radius: 6px;
  padding: 0.85rem 1rem 0.95rem;
  min-width: 0;
}
body.dash .tile:has(.tile-value.up),
body.dash .tile:has(.tile-value.fell) { border-left-color: var(--emerald); }
body.dash .tile:has(.tile-value.down),
body.dash .tile:has(.tile-value.rose) { border-left-color: var(--warn); }
/* Nothing in it yet is not a state worth colouring. */
body.dash .tile:has(.tile-empty) { border-left-color: var(--line); }
body.dash .tile-label {
  font-size: var(--step--2);
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.05em;
  line-height: 1.25;
  color: var(--muted);
  margin-bottom: 0.4rem;
}
body.dash .tile-value {
  font-size: clamp(var(--step-3), 4.2vw, var(--step-4));
  font-weight: 700;
  line-height: 1.05;
  letter-spacing: -0.02em;
  color: var(--navy);
  white-space: nowrap;
}
body.dash .tile-note {
  font-size: var(--step--1);
  line-height: 1.45;
  color: var(--muted);
  margin-top: 0.35rem;
}
body.dash .tile-empty { font-size: var(--step-0); color: var(--muted); font-style: italic; }
body.dash .tile-value.up { color: var(--emerald); }
body.dash .tile-value.down { color: var(--danger); }
/* The arrow is drawn, not typed into every template.
   up and down are for a figure where rising is the good direction: attendance,
   giving. rose and fell are for one where it is not, such as a queue of work
   orders getting longer. The ARROW follows the sign and the COLOUR follows
   whether it is good news, because an arrow pointing down beside "+1" is a
   sentence contradicting itself. */
body.dash .tile-value.up::before   { content: "\2191\00a0"; font-size: 0.80em; }
body.dash .tile-value.down::before { content: "\2193\00a0"; font-size: 0.80em; }
body.dash .tile-value.rose::before { content: "\2191\00a0"; font-size: 0.80em; }
body.dash .tile-value.fell::before { content: "\2193\00a0"; font-size: 0.80em; }
body.dash .tile-value.rose { color: var(--danger); }
body.dash .tile-value.fell { color: var(--emerald); }
/* The one that wants an answer today. Still white, so the row stays one family;
   the edge and the figure carry it. A filled red box beside four white ones is
   louder than the thing it is reporting. */
body.dash .tile.alarm { border-left-color: var(--danger); }
body.dash .tile.alarm .tile-label { color: var(--danger); }
body.dash .tile.alarm .tile-value { color: var(--danger); }

@media (max-width: 640px) {
  body.dash .tile-value { font-size: var(--step-3); }
}

/* Entering a number happens at the foot of the tab that shows it. Eric,
   2026-08-20: "the tabs need to have the data metrics at the top, and the bottom
   is where you can enter data. that's where it gets recorded." */
.entrybox {
  border: 1px solid var(--line);
  border-top: 3px solid var(--emerald);
  border-radius: 6px;
  background: #fff;
  padding: 1rem 1.2rem 1.2rem;
  margin: 0 0 1rem;
}
.entrybox h2 { font-size: var(--step-1); letter-spacing: -0.01em;
              margin-bottom: 0.2rem; }
.entrybox .lede { margin-bottom: 0.9rem; }
.entrybox table.entry { margin: 0 0 0.8rem; }
.entrybox .readonly {
  color: var(--muted);
  font-style: italic;
  margin: 0;
}
.entryrow { display: flex; flex-wrap: wrap; gap: 0.75rem; align-items: flex-end; }
.entryrow label { display: flex; flex-direction: column; gap: 0.25rem; margin: 0;
                  font-size: var(--step--1); color: var(--muted); }
.entryrow input, .entryrow select { min-width: 8rem; }

/* Modular, responsive boxes. Eric, 2026-08-20: "The boxes should be modular.
   they should be responsive."

   The dashboard's cards were a single column of full-width blocks, so a wide
   screen showed one card and a lot of empty page, and reading two things side by
   side meant scrolling between them. They sit in a twelve column grid now: a
   card claims half the width when its content holds up at that size, the whole
   width when it needs the room for a chart, and every card falls back to the
   full width on a narrow screen. Nothing here is a fixed pixel size, so the
   layout has no breakpoint it looks wrong just below. */
body.dash main {
  display: grid;
  grid-template-columns: repeat(12, minmax(0, 1fr));
  gap: 1.1rem;
  align-content: start;
}
/* Headings, tabs, the gap strip and anything else that is not a box run the
   full width of the grid. */
body.dash main > *:not(.card):not(.entrybox) { grid-column: 1 / -1; }
body.dash main > .card,
body.dash main > .entrybox { grid-column: 1 / -1; margin-bottom: 0; }
body.dash main > .card.half,
body.dash main > .entrybox.half { grid-column: span 6; }

@media (max-width: 60rem) {
  body.dash main > .card.half,
  body.dash main > .entrybox.half { grid-column: 1 / -1; }
}
@media (max-width: 48rem) {
  main { width: auto; max-width: none; padding-left: 1rem; padding-right: 1rem; }
  body.wide main { width: auto; max-width: none; }
}

/* A tile row inside a half-width card holds two across rather than trying for
   four in half the space. */
body.dash .card.half .tile,
body.dash .entrybox.half .tile { flex-basis: 8.5rem; }

/* The other St. Edward apps, from the masthead. A details element, so it opens
   with no script and closes with Escape, and it sits in the bar rather than on a
   page, because the reason to leave for one of them can arrive on any screen. */
.appsmenu { position: relative; }
.appsmenu > summary {
  list-style: none;
  cursor: pointer;
  color: #e9e6dd;
  border-bottom: 2px solid transparent;
  padding-bottom: 2px;
}
.appsmenu > summary::-webkit-details-marker { display: none; }
.appsmenu > summary::after { content: " \25be"; }
.appsmenu > summary:hover,
.appsmenu[open] > summary { color: #fff; border-bottom-color: var(--gold); }
.appsmenu > summary:focus-visible { outline: 2px solid var(--gold); outline-offset: 3px; }
.appsmenu ul {
  position: absolute;
  right: 0;
  top: calc(100% + 0.6rem);
  z-index: 30;
  min-width: 17rem;
  margin: 0;
  padding: 0.4rem;
  list-style: none;
  background: #fff;
  border: 1px solid var(--line);
  border-top: 3px solid var(--gold);
  border-radius: 6px;
  box-shadow: 0 8px 24px rgba(20, 32, 46, 0.22);
}
.appsmenu li { display: flex; align-items: baseline; gap: 0.5rem; padding: 0.35rem 0.5rem; }
/* The panel is white and it sits inside <nav>, where `nav a` paints links the
   near-white that belongs on the navy bar. Without this the menu rendered white
   on white: the same defect as the brand, in the thing built to fix the brand.
   A link inside a panel takes the panel's colours, not the bar's. */
.appsmenu li a { color: var(--emerald); text-decoration: none; font-weight: 600; }
.appsmenu li a:hover, .appsmenu li a:focus-visible {
  color: var(--navy); text-decoration: underline;
}

/* On a phone the bar wraps, so the menu drops into the flow rather than floating
   off the right edge of a narrow screen. */
@media (max-width: 48rem) {
  .appsmenu ul { position: static; min-width: 0; box-shadow: none; margin-top: 0.4rem; }
}

/* ==========================================================================
   The ledger. Every number, and the place a wrong one gets put right.
   ==========================================================================
   Rows are rows. The editor opens on the row being fixed rather than sitting
   open on all two hundred of them, which is what made this page a wall of input
   boxes with the data as the smallest thing on it. */
.ledgerfilter {
  display: flex; flex-wrap: wrap; gap: 1rem; align-items: flex-end;
  margin: 0 0 1.2rem;
}
.ledgerfilter label { display: flex; flex-direction: column; gap: 0.25rem;
                      margin: 0; font-size: var(--step--2); color: var(--muted); }
.ledgerfilter select { min-width: 12rem; }

.ledger { border: 1px solid var(--line); border-radius: 8px; background: #fff;
          overflow: hidden; }

/* One column template, used by the header and by every row's summary, so the
   two cannot drift apart. */
.ledger-head, .ledger-row > summary {
  display: grid;
  /* THE SHARES BELOW ARE MEASURED, not guessed. At 1440 the ledger is 1080px
     wide, and after the padding, the six gaps and the fixed control there are
     873px for the six flexible columns. What each one actually has to hold, at
     the size it holds it: the longest metric label, "Offertory in LGL, cash and
     check", is 264px; a date is 147px and the Mass under it, "Saturday 5:00 PM
     Vigil", is 163px; the widest money value is 97px; a name is 107px; "416
     days later" is 100px; "live" beside "edited" is about 107px. The fractions
     hand every column its measurement plus a few pixels, and the surplus goes
     to the label, which is the only one of the six whose content grows when
     somebody adds a metric to the registry.

     The old 2.2/1.3/0.7/0.9/0.7/0.9 gave the label 290px and the lag 92px, so
     the label wrapped as soon as a period phrase followed it and "416 days
     later" wrapped on every annual row. Both defects were a column measured by
     eye against content nobody had measured.

     Minimums in rem, against an 18pt root, so these are far bigger than they
     read. They are what the columns fall back to between 1440 and the 60rem
     breakpoint below, where there is not enough room for all seven to have what
     they want. The first version summed to 50rem, wider than the 80vw column,
     and the row overflowed: the Edit control was pushed off the right edge of
     the card and could not be clicked. The set after that summed to 32.9rem,
     which is 725px once the gaps and the padding are counted, and still
     overflowed the 697px ledger of a 961px window, the narrowest one this grid
     is ever used at, where `overflow: hidden` on the card silently clipped the
     right-hand end of every row. These sum to 30.7rem, 553px, which leaves that
     window 11px of room.

     The value column's minimum is the odd one out and it is deliberate: a label
     or a name or a date can wrap when a column gets tight, and "$12,860.56"
     cannot, so a column narrower than the widest amount this ledger holds does
     not wrap the number, it clips a digit off the end of it. 5.6rem is that
     widest amount, measured, plus a little. Six figures would need this raised
     again. */
  grid-template-columns:
    minmax(6rem, 2.2fr)     /* what */
    minmax(5rem, 1.35fr)    /* when */
    minmax(5.6rem, 0.8fr)   /* value */
    minmax(3.6rem, 0.9fr)   /* who */
    minmax(3rem, 0.82fr)    /* lag */
    minmax(3.4rem, 0.9fr)   /* status */
    4.1rem;                 /* the control, sized in the .open rule below */
  gap: 0.9rem;
  align-items: baseline;
  padding: 0.7rem 1rem;
}
.ledger-head {
  background: var(--parchment);
  border-bottom: 1px solid var(--line);
  font-size: var(--step--2); text-transform: uppercase; letter-spacing: 0.06em;
  color: var(--muted);
}
.ledger-row { border-bottom: 1px solid var(--line); }
.ledger-row:last-child { border-bottom: 0; }
.ledger-row > summary { cursor: pointer; list-style: none; }
.ledger-row > summary::-webkit-details-marker { display: none; }
.ledger-row > summary:hover { background: var(--parchment); }
.ledger-row > summary:focus-visible { outline: 2px solid var(--emerald);
                                      outline-offset: -2px; }

/* A ROW IS A TWO-LINE BOX, and the second line is reserved whether or not
   anything fills it. Left to its content, a row was one line tall when the
   metric was called "Confessions" and two when it was called "Offertory in LGL,
   cash and check", because the period phrase after the label ran past the
   label's column and wrapped. Every second row in the list was then a different
   height from the one above it, and a list whose rhythm changes with the length
   of a name reads as a broken table rather than as a list.

   Two body line boxes plus the summary's own vertical padding. Two lines is
   what the tallest ordinary row needs, a fact and its qualifier, so reserving
   the second one costs nothing on the rows that only use one, and it also
   absorbs the cases the columns cannot be sized against in advance: a person
   with a long name, a lag that runs to two lines. Written off the scale rather
   than as a pixel count so it follows the type if the type ever moves. */
.ledger-row > summary { min-height: calc(2 * 1.55 * var(--step-0) + 1.4rem); }

/* The qualifier under a fact is a SECOND LINE, not the tail of the first one.
   Inline, the period phrase, the Mass and the correction reason each ran on
   from the thing they belong to and broke wherever the column happened to end,
   which is what decided a row's height. On its own line each one reads as what
   it is, and it lands on the line the box above already reserved. `.muted`
   already takes them a step down; nothing here sets a size. */
.ledger-row .what > .muted,
.ledger-row .when > .muted,
.ledger-row .state > .muted { display: block; }
.ledger-row .num, .ledger-head .num { text-align: right; }
/* NOT .who: that class is the masthead's name, painted gold-sand for the navy
   bar, and reusing it here printed the name in pale gold on white. */
.ledger-row .by { color: var(--ink); }
/* A "never counted" row carries a badge where a number goes, and the badge is
   wider than any amount: 142px of nowrap capitals in a column sized for
   "$12,860.56", so 41px of it lay across the name in the column beside it. It
   wraps inside its own column instead. The badge is then two lines deep, and an
   inline-block takes its baseline from its LAST line, so a wrapped badge would
   drag the whole row down behind it; this column steps out of the row's
   baseline to stop that. Every other value here is a single line of digits at
   body size, whose baseline sits in the same place under either alignment, so
   nothing else moves. Those rows are still the tallest on the page, and that is
   not this: a closed gap also prints the reason it was closed, which is a
   second sentence no other row carries. */
.ledger-row .value { font-weight: 600; font-variant-numeric: tabular-nums;
                     align-self: start; }
.ledger-row .value .not-counted { white-space: normal; }
.ledger-row .open {
  text-align: right; color: var(--emerald); font-weight: 600; font-size: var(--step--1);
  /* The caret is generated content introduced by a space, and a space is a
     break opportunity. In a 3.4rem column, 61px, "Details" measured 52px and
     the caret another 11, so the triangle dropped onto a line of its own
     underneath the word and the last column read as "Details" with a lonely
     arrow below it. "Edit" is short enough that it never showed the fault. The
     column is 4.1rem now, which clears the longer of the two words with its
     caret, and this keeps word and arrow together whatever is put in here
     next. */
  white-space: nowrap;
}
.ledger-row[open] > summary { background: var(--parchment); }
.ledger-row[open] .open::after { content: " \25b4"; }
.ledger-row:not([open]) .open::after { content: " \25be"; }
.ledger-row.superseded .value, .ledger-row.voided .value { text-decoration: line-through; }
.ledger-row.superseded, .ledger-row.voided { color: var(--muted); }

.pill {
  display: inline-block; padding: 0.1rem 0.5rem; border-radius: 999px;
  font-size: var(--step--2); font-weight: 600; white-space: nowrap;
}
.pill-active { background: #e4efe8; color: var(--emerald); }
.pill-superseded { background: #efe9dc; color: var(--warn); }
.pill-voided { background: #f4e2e0; color: var(--danger); }
/* A closed gap is a settled fact, not a verdict on a number, so it wears the
   neutral ink on the page's own line colour rather than joining the good,
   caution and bad scale the three above it form. Ink on that ground measures
   9.86:1. Without a rule of its own it drew as bare text beside real pills and
   read as though the row had no status at all. */
.pill-not_counted { background: #eae5d6; color: var(--navy); }

/* AN ESTIMATE IS NOT A COUNT, and the difference has to survive a black and
   white printout carried into a meeting, so it is said in a word rather than
   shown in a colour. The figure keeps its size, because it is a real answer to
   a real question; the word beside it is what stops it being read as measured. */
.estimate { font-weight: 600; font-style: italic; }
.estimate-mark {
  display: inline-block; margin-left: 0.45rem;
  font-size: var(--step--2); letter-spacing: 0.08em; text-transform: uppercase;
  color: var(--warn); border: 1px solid var(--warn); padding: 0.1rem 0.3rem;
  vertical-align: 0.1em;
}
table.entry.estimated th[scope="row"] { font-weight: 400; }
.pill-edited { background: #efe9dc; color: var(--warn); }

.ledger-edit {
  padding: 0.4rem 1rem 1.3rem;
  background: var(--parchment);
  border-top: 1px dashed var(--line);
}
.ledger-edit h3 { font-size: var(--step--1); margin: 1rem 0 0.15rem; }
.ledger-edit form + form { border-top: 1px solid var(--line); margin-top: 1.1rem; }
.ledger-edit .entryrow { margin-top: 0.5rem; }
.ledger-edit .entryrow .grow { flex: 1 1 18rem; }
.ledger-edit button.danger { background: var(--danger); }
.ledger-meta {
  display: grid; grid-template-columns: max-content 1fr; gap: 0.2rem 0.9rem;
  margin: 1.1rem 0 0; font-size: var(--step--1);
}
.ledger-meta dt { color: var(--muted); }
.ledger-meta dd { margin: 0; }

/* The whole grid collapses to labelled lines on a narrow screen: seven columns
   in 390px is seven columns nobody can read. */
@media (max-width: 60rem) {
  .ledger-head { display: none; }
  .ledger-row > summary {
    grid-template-columns: 1fr auto;
    gap: 0.2rem 0.8rem;
    padding: 0.9rem 1rem;
  }
  .ledger-row .what { grid-column: 1; font-weight: 600; }
  .ledger-row .open { grid-column: 2; grid-row: 1; }
  /* .by, not .who. The rule above this block already says why the ledger's
     name column cannot be called .who, and this line was never updated with it,
     so it matched nothing: the name fell to auto-placement, landed in column 2
     beside the value, and read on a phone as though "Carlli Kloss" were part of
     the figure rather than the person who typed it. */
  .ledger-row .when, .ledger-row .by, .ledger-row .lag { grid-column: 1; }
  .ledger-row .value { grid-column: 1; font-size: var(--step-1); text-align: left; }
  .ledger-row .state { grid-column: 1; }
}

/* Supporting prose inside a card sits a step below body. It is there for the
   reader who doubts a number, and it was competing with the number itself. */
body.dash .card p,
body.dash .entrybox p { font-size: var(--step--1); line-height: 1.55; }
body.dash .card p.total { font-size: var(--step-3); }
body.dash .blurb { font-size: var(--step-0); }

/* A Mass name in an entry row is a label, not a headline. It was set at body
   size beside a card title barely larger than it. */
.entrybox table.entry th { font-size: var(--step--1); font-weight: 600; }
.entrybox table.entry td { font-size: var(--step-0); }

/* A caveat that belongs to a number, kept next to it and out of the way. It was
   four lines of body prose sitting between the figures and the chart. */
.aside { margin: .6rem 0 1.2rem; }
.aside > summary {
  cursor: pointer;
  font-size: var(--step--1);
  color: var(--muted);
  list-style: none;
}
.aside > summary::-webkit-details-marker { display: none; }
.aside > summary::after { content: " \25be"; }
.aside[open] > summary::after { content: " \25b4"; }
.aside > summary:hover { color: var(--ink); }
.aside p { font-size: var(--step--1); color: var(--muted); margin: .5rem 0 0; }


/* ---------------------------------------------------------------------------
   NARROW SCREENS. Last in the file on purpose. `table.entry th[scope="row"]`
   further up sets white-space: nowrap at exactly the same specificity as the
   rule below that clears it, so source order is the whole argument. Sitting
   earlier, this block applied its widths, did NOT apply its wrapping, and
   clipped "Saturday 5:00 PM Vigil" to "Saturday 5:00 PM Vi".
   --------------------------------------------------------------------------- */
/* The weekend sheet on a phone. Two numbers per Mass beside a Mass name is the
   paper sheet's own shape and it is worth keeping, so this narrows the parts
   rather than restacking the grid: the ministers read down a column, which is
   why the tab order walks down one, and a per-Mass card would break that
   reading as well as that typing.
   At 390px the sheet measured 588px wide and the offertory column sat entirely
   off the right edge, on the one screen the count team and the hospitality
   ministers actually use. */
@media (max-width: 480px) {
  /* Fixed layout, and the widths below are what make it right. Left to auto,
     the browser gives the name column its longest line as a minimum, which put
     the table back over the window even with wrapping allowed. Fixed reads the
     widths off the header row, so the two number columns are pinned and the
     name takes whatever is left and wraps into it. */
  table.entry { width: 100%; table-layout: fixed; }
  table.entry th[scope="col"], table.entry td, table.entry th[scope="row"] {
    padding-left: 0.3rem;
    padding-right: 0.3rem;
  }
  /* The Mass name WRAPS now instead of holding the column open on one line.
     Equal thirds was the first attempt and was wrong: a name needs more room
     than a number box, and fixed layout clipped "Saturday 5:00 PM Vigil" to
     "Saturday 5:00". The number columns are pinned instead and the name takes
     whatever is left. */
  table.entry td.mass, table.entry th.mass { white-space: normal; }
  table.entry th.mass strong { font-weight: 600; }
  /* The date can afford to be small: the page heading already says which
     weekend this is, and the day is in the Mass name. */
  table.entry th.mass span { font-size: var(--step--2); }
  table.entry td.count, table.entry th.count { width: 4.6rem; }
  table.entry td.money, table.entry th.money { width: 5.4rem; }
  table.entry td.count input, table.entry td.money input { width: 100%; }
  /* The dollar sign is decoration next to a field already in decimal mode, and
     it costs a character of a field somebody is typing money into. */
  table.entry .dollar { display: none; }
  /* The totals row carries a whole sentence per column at this width. */
  table.entry tr.totals .no-total { font-size: var(--step--2); }
  /* A recorded number is text standing where an input was, and it kept the
     input's old fixed minimum: 4.5rem plus a rem of padding, inside a column
     pinned to 4.6rem. On the single-Mass screen that put the table at 405px in
     a 390px window. It takes the column it sits in instead. Found by
     check_widths.py the first time that check ran against a database with
     numbers in it. */
  table.entry .recorded { min-width: 0; width: 100%; padding: 0.4rem 0.15rem; }
  /* The metric name on the single-Mass screen is a th[scope="row"] carrying no
     .mass class, so the wrapping rule above never matched it and "Mass
     attendance" held its column open on one line. Every row header wraps at
     this width; the weekend sheet's already did. */
  table.entry th[scope="row"] { white-space: normal; }
}


/* The ledger's pager. Two links and a position, because the only two moves
   anybody makes here are one step further back and one step forward.

   No rule above it any more. The border-top was drawn on both copies of the
   pager: on the one at the top of the list it landed between the column
   headings and the first entry and drew the exact line a row separator draws,
   which is half of why a pager read as data, and on the one below the card it
   doubled up with the card's own bottom border. The card's border does the
   separating on this page; the pager only needs air. */
.pager {
  display: flex; align-items: baseline; gap: 1rem;
  margin: 1.1rem 0 0;
}
.pager a { font-weight: 600; }
.pager-where { color: var(--muted); font-size: var(--step--1); }

/* The count sentence and the top pager, on one line above the card. They are
   the same fact from two directions, which slice of the record is on screen and
   how to reach the next one, so they belong beside each other rather than
   stacked, and above the numbers rather than among them. Under about 34rem they
   wrap to two lines and the count keeps the first one. */
.ledger-topbar {
  display: flex; flex-wrap: wrap; align-items: baseline;
  justify-content: space-between; gap: 0.2rem 1.5rem;
  margin: 0 0 0.6rem;
}
.ledger-topbar .pager { margin: 0; }
.ledger-count { margin: 0; }
