/* Thebes interface. Papyrus and ink, teal accents, coral for problems, amber for warnings (spec 6.1); gold on lapis for the
   main action, the frieze's lapis, gold, terracotta and turquoise, and cartouche headings in a serif (spec 6.9). */
:root {
  --parchment: #F3E7CB; --parchment-2: #EADBB9; --parchment-3: #DCC9A0;
  --ink: #2B1F0E; --ink-soft: #5A4832;
  --teal: #1F6F66; --teal-ink: #FFFFFF; --teal-soft: #CFE6E2;
  --coral: #E2604C; --coral-text: #A33624; --coral-soft: #F6D5CE;
  --amber: #E0A030; --amber-text: #7A5200; --amber-soft: #F6E7C2;
  --blue: #2F6FA8; --gold: #E2B13C;
  --lapis: #1C3768; --lapis-ink: #F5D98A; --terracotta: #B4502E; --turquoise: #3A9E96; --ink-line: #8C7550;   /* gold on lapis 8.4:1; the ink outline 3.6:1 on parchment */
  --serif: "Palatino Linotype", Palatino, "Book Antiqua", Georgia, serif;   /* the display face for titles, from the system's fonts: nothing is downloaded */
  /* Papyrus fibre (spec 6.9): a 160x120 tile of fine strokes. Two layers are lighter than the parchment (the strips laid one
     way, then the other); one is darker, and its group opacity caps it, so no point of the tile is darker than the parchment
     blended 7% with #A9844C, which keeps every text pair at 4.5:1 (tests/stage4/look/lookBase.test.js reads this value). */
  --look-fibre: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='160' height='120'%3E%3Cg fill='none' stroke='%23FFF9EA' opacity='0.5'%3E%3Cpath d='M0 3.5c53 0.2 107 -0.2 160 0' stroke-opacity='0.45' stroke-width='1.2'/%3E%3Cpath d='M0 5.5c53 -0.2 107 0.2 160 0' stroke-opacity='1' stroke-width='0.6'/%3E%3Cpath d='M0 7.5c53 -0.6 107 0.6 160 0' stroke-opacity='1' stroke-width='0.8'/%3E%3Cpath d='M0 11.5c53 -0.3 107 0.3 160 0' stroke-opacity='0.8' stroke-width='0.6'/%3E%3Cpath d='M0 16.5c53 0.4 107 -0.4 160 0' stroke-opacity='0.8' stroke-width='0.6'/%3E%3Cpath d='M0 18.5c53 0.1 107 -0.1 160 0' stroke-opacity='0.45' stroke-width='0.6'/%3E%3Cpath d='M0 22.5c53 -0.5 107 0.5 160 0' stroke-opacity='0.3' stroke-width='0.8'/%3E%3Cpath d='M0 29.5c53 -0.1 107 0.1 160 0' stroke-opacity='0.45' stroke-width='1'/%3E%3Cpath d='M0 34.5c53 0.1 107 -0.1 160 0' stroke-opacity='0.3' stroke-width='1'/%3E%3Cpath d='M0 40.5c53 0.1 107 -0.1 160 0' stroke-opacity='0.45' stroke-width='0.6'/%3E%3Cpath d='M0 46.5c53 -0.5 107 0.5 160 0' stroke-opacity='0.45' stroke-width='1'/%3E%3Cpath d='M0 52.5c53 0.1 107 -0.1 160 0' stroke-opacity='0.3' stroke-width='0.6'/%3E%3Cpath d='M0 56.5c53 0.3 107 -0.3 160 0' stroke-opacity='1' stroke-width='1.2'/%3E%3Cpath d='M0 60.5c53 -0.2 107 0.2 160 0' stroke-opacity='1' stroke-width='1.2'/%3E%3Cpath d='M0 63.5c53 -0.5 107 0.5 160 0' stroke-opacity='0.45' stroke-width='0.8'/%3E%3Cpath d='M0 66.5c53 0.5 107 -0.5 160 0' stroke-opacity='1' stroke-width='1.2'/%3E%3Cpath d='M0 72.5c53 0.1 107 -0.1 160 0' stroke-opacity='0.8' stroke-width='1'/%3E%3Cpath d='M0 74.5c53 -0.4 107 0.4 160 0' stroke-opacity='0.3' stroke-width='1.2'/%3E%3Cpath d='M0 77.5c53 -0.1 107 0.1 160 0' stroke-opacity='0.45' stroke-width='1.2'/%3E%3Cpath d='M0 83.5c53 -0.2 107 0.2 160 0' stroke-opacity='0.3' stroke-width='1'/%3E%3Cpath d='M0 86.5c53 0.1 107 -0.1 160 0' stroke-opacity='1' stroke-width='1.2'/%3E%3Cpath d='M0 90.5c53 0.5 107 -0.5 160 0' stroke-opacity='0.3' stroke-width='0.6'/%3E%3Cpath d='M0 94.5c53 0.3 107 -0.3 160 0' stroke-opacity='0.3' stroke-width='0.6'/%3E%3Cpath d='M0 97.5c53 -0.3 107 0.3 160 0' stroke-opacity='1' stroke-width='1.2'/%3E%3Cpath d='M0 101.5c53 0.5 107 -0.5 160 0' stroke-opacity='0.6' stroke-width='0.6'/%3E%3Cpath d='M0 104.5c53 -0.0 107 0.0 160 0' stroke-opacity='0.45' stroke-width='0.6'/%3E%3Cpath d='M0 107.5c53 0.3 107 -0.3 160 0' stroke-opacity='0.6' stroke-width='0.8'/%3E%3Cpath d='M0 111.5c53 -0.5 107 0.5 160 0' stroke-opacity='0.8' stroke-width='1.2'/%3E%3Cpath d='M0 115.5c53 0.5 107 -0.5 160 0' stroke-opacity='0.8' stroke-width='1'/%3E%3C/g%3E%3Cg fill='none' stroke='%23FFF9EA' stroke-width='1.5' opacity='0.22'%3E%3Cpath d='M21.5 0v120' stroke-opacity='0.4'/%3E%3Cpath d='M28.5 0v120' stroke-opacity='0.4'/%3E%3Cpath d='M49.5 0v120' stroke-opacity='0.7'/%3E%3Cpath d='M58.5 0v120' stroke-opacity='1'/%3E%3Cpath d='M70.5 0v120' stroke-opacity='0.7'/%3E%3Cpath d='M91.5 0v120' stroke-opacity='0.7'/%3E%3Cpath d='M107.5 0v120' stroke-opacity='0.4'/%3E%3Cpath d='M114.5 0v120' stroke-opacity='0.7'/%3E%3Cpath d='M130.5 0v120' stroke-opacity='0.7'/%3E%3Cpath d='M146.5 0v120' stroke-opacity='0.7'/%3E%3Cpath d='M153.5 0v120' stroke-opacity='0.4'/%3E%3C/g%3E%3Cg fill='none' stroke='%23A9844C' opacity='0.07'%3E%3Cpath d='M27 15.5h73' stroke-opacity='1'/%3E%3Cpath d='M85 26.5h73' stroke-opacity='1'/%3E%3Cpath d='M17 34.5h56' stroke-opacity='0.5'/%3E%3Cpath d='M47 42.5h79' stroke-opacity='0.5'/%3E%3Cpath d='M0 53.5h84' stroke-opacity='0.75'/%3E%3Cpath d='M54 64.5h102' stroke-opacity='1'/%3E%3Cpath d='M86 69.5h64' stroke-opacity='0.75'/%3E%3Cpath d='M38 83.5h39' stroke-opacity='0.75'/%3E%3Cpath d='M18 97.5h140' stroke-opacity='0.5'/%3E%3Cpath d='M11 111.5h102' stroke-opacity='0.5'/%3E%3Cpath d='M54 116.5h71' stroke-opacity='0.5'/%3E%3C/g%3E%3C/svg%3E");
  --shadow: 0 4px 16px rgba(43, 31, 14, 0.25);
  --radius: 12px; --tap: 44px;
  --ui-scale: 1;
  --font: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
  --dock-tucked: 52px;
  --panel-w: 320px; --rail-w: 56px; --drawer-w: 268px;
  --safe-top: env(safe-area-inset-top, 0px); --safe-bottom: env(safe-area-inset-bottom, 0px);
  --safe-left: env(safe-area-inset-left, 0px); --safe-right: env(safe-area-inset-right, 0px);
}
[data-text-size="125"] { --ui-scale: 1.25; }
[data-text-size="150"] { --ui-scale: 1.5; }
/* Derived sizes live on body, where --ui-scale is set, so they scale with the text-size setting. */
body { --topbar-h: calc(48px * var(--ui-scale)); --dock-slim: calc(76px * var(--ui-scale)); --dock-tray: calc(232px * var(--ui-scale)); }

html, body { margin: 0; height: 100%; background: var(--parchment-3); color: var(--ink); overflow: hidden; overscroll-behavior: none; -webkit-text-size-adjust: 100%; }
body { font: calc(16px * var(--ui-scale)) / 1.4 var(--font); }
#map { position: fixed; inset: 0; width: 100vw; height: 100vh; height: 100dvh; display: block; touch-action: none; }
#ui { position: fixed; inset: 0; pointer-events: none; }
#ui > * { pointer-events: auto; }
[hidden] { display: none !important; }

button { font: inherit; color: inherit; background: var(--parchment); border: 1px solid var(--ink-line); border-radius: var(--radius); min-width: var(--tap); min-height: var(--tap); padding: 0 12px; cursor: pointer; }
/* Gold on lapis for the one main action of a sheet or bar (spec 6.9), with a hairline of gold inside its edge. A row's own
   action in a sheet's list (each need's, goal's or route's) is lapis words in a lapis outline, so the sheet keeps one. */
button.primary { background: var(--lapis); color: var(--lapis-ink); border-color: var(--lapis); box-shadow: inset 0 0 0 2px var(--lapis), inset 0 0 0 3px rgba(245, 217, 138, 0.5); }
#sheet li button.primary { background: var(--parchment); color: var(--lapis); border-color: var(--lapis); box-shadow: none; }
button.danger { background: var(--coral-text); color: #FFFFFF; border-color: var(--coral-text); }
button.quiet { background: transparent; border-color: transparent; }
button[disabled] { opacity: 0.5; cursor: default; }
button[aria-pressed="true"] { border-color: var(--teal); box-shadow: inset 0 0 0 2px var(--teal); }
:focus-visible { outline: 3px solid var(--teal); outline-offset: 2px; }
h2, h3 { margin: 0; }
.sr-only { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; }
.small { font-size: calc(12px * var(--ui-scale)); }   /* 12 px is the floor (spec 6.1) */
.muted { color: var(--ink-soft); }
.coral { color: var(--coral-text); }
.amber { color: var(--amber-text); }
.teal { color: var(--teal); }
.icon { display: inline-block; width: 1.25em; text-align: center; }
.row { display: flex; justify-content: space-between; gap: 12px; padding: 6px 0; border-bottom: 1px solid var(--parchment-2); }
.actions { display: flex; gap: 8px; padding-top: 12px; flex-wrap: wrap; }
.noscript { margin: 24px; }

/* Top bar: things you read sit at the top. */
#topbar { position: fixed; top: 0; left: 0; right: 0; padding: var(--safe-top) max(8px, var(--safe-right)) 0 max(8px, var(--safe-left)); min-height: var(--topbar-h); background: var(--parchment); border-bottom: 1px solid var(--parchment-3); box-shadow: var(--shadow); display: flex; align-items: center; gap: 6px; }
#topbar .stat { display: flex; align-items: center; gap: 3px; white-space: nowrap; }
/* The numbers keep to one row wherever they fit (every layout at 100% text, with tight 8 px gaps and 4 px ends); only
   where they would otherwise be cut off (a narrow phone at large text, or very large numbers) do they wrap to a second row, inside the bar's
   own height. In the wide layout under 1000 px wide or at 150% text the date is short (createUI, hud.js), so the numbers never need a third row. */
#topbar .stats { display: flex; flex-wrap: wrap; align-items: center; align-content: center; gap: 0 8px; flex: 1; min-width: 0; overflow: hidden; line-height: 1.2; padding: 0 4px; }
#topbar .stat .label { font-size: calc(12px * var(--ui-scale)); color: var(--ink-soft); }
#topbar .spacer { display: none; }   /* the numbers' flex: 1 already pushes the rest right; a spacer would only cost a gap */
#topbar .speed { display: flex; gap: 4px; }
#topbar button { min-height: var(--tap); }
body[data-layout="portrait"] .wide-only, body[data-layout="compact"] .wide-only { display: none !important; }
body[data-layout="compact"] { --topbar-h: calc(40px * var(--ui-scale)); }

/* Critical banner, overlay legend and goals strip stack under the top bar in one column, so nothing overlaps whatever the text size. On a phone only the alert row and one guidance row show (the guidance row rules below). */
#under-bar { position: fixed; left: max(8px, var(--safe-left)); right: max(8px, var(--safe-right)); top: calc(var(--safe-top) + var(--topbar-h) + 6px); display: flex; flex-direction: column; gap: 6px; align-items: flex-start; pointer-events: none; }
#banner, #legend { position: static; display: flex; flex-direction: column; gap: 6px; align-items: flex-start; pointer-events: none; max-width: 100%; }
#under-bar > :empty { display: none; }
#banner > *, #legend > * { pointer-events: auto; }
#banner .critical { background: var(--coral-soft); border: 1px solid var(--coral-text); border-radius: var(--radius); padding: 6px 8px 6px 12px; display: flex; align-items: center; gap: 8px; max-width: 100%; }
#legend .strip { display: inline-flex; align-items: center; gap: 10px; background: var(--parchment); border-radius: var(--radius); padding: 4px 6px 4px 12px; box-shadow: var(--shadow); }
#legend .dot, #legend .ring { display: inline-block; width: 12px; height: 12px; border-radius: 50%; box-sizing: border-box; }
#legend .dot { background: currentColor; }
#legend .ring { border: 2px solid currentColor; }
/* On a wide screen under a banner the legend shrinks to its dot and ring; the words stay for screen readers (sr-only). A
   graded scale (the desirability bands) keeps its name and labels: swatches alone would be colour alone (spec 6.1). A
   phone's legend has the guidance row to itself, so it keeps its words. */
body.has-banner[data-layout="wide"] #legend .strip:not(.graded) .text { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; }
body[data-hand="left"] .confirm { flex-direction: row-reverse; }

/* Corner buttons: speed bottom-left, minimap and overlay bottom-right. Never in the middle. */
#corners { position: fixed; left: var(--safe-left); right: var(--safe-right); bottom: calc(var(--safe-bottom) + var(--dock-slim) + 8px); display: flex; justify-content: space-between; padding: 0 8px; pointer-events: none; }
#corners > * { pointer-events: auto; }
#corners .group { display: flex; gap: 8px; }
body[data-dock="tray"] #corners { bottom: calc(var(--safe-bottom) + var(--dock-tray) + 8px); }
body[data-dock="tucked"] #corners { bottom: calc(var(--safe-bottom) + var(--dock-tucked) + 8px); }
body[data-hand="left"] #corners { flex-direction: row-reverse; }
#corners.over-half { visibility: hidden; }   /* the portrait tray is open and tall: the corners would sit in the middle of the map (corners.js) */
.has-badge { position: relative; }
.badge { position: absolute; top: -6px; right: -6px; min-width: 20px; height: 20px; border-radius: 10px; background: var(--coral-text); color: #FFFFFF; font-size: 12px; line-height: 20px; text-align: center; padding: 0 5px; }

/* Portrait build dock: slim bar, tray, or tucked to a pill (spec 6.3). */
#dock { position: fixed; left: 0; right: 0; bottom: 0; background: var(--parchment); border-top: 1px solid var(--parchment-3); border-radius: 16px 16px 0 0; box-shadow: var(--shadow); padding-bottom: var(--safe-bottom); display: flex; flex-direction: column; }
#dock .handle { align-self: center; width: 44px; height: auto; margin: -10px 0; border: none; background: transparent; min-width: 0; min-height: var(--tap); padding: 0; box-shadow: none; touch-action: none; }
#dock .handle::before { content: ""; display: block; width: 40px; height: 5px; border-radius: 3px; background: var(--parchment-3); margin: 8px auto; }
#dock .cats { display: flex; justify-content: space-around; padding: 0 4px 3px; }
#dock .cat { display: flex; flex-direction: column; align-items: center; gap: 2px; border: none; background: none; min-width: var(--tap); padding: 2px 4px; font-size: calc(12px * var(--ui-scale)); border-radius: 8px; box-shadow: none; }
#dock .cat[aria-pressed="true"] { color: var(--teal); font-weight: 600; box-shadow: none; }
#dock .cat .icon { font-size: calc(20px * var(--ui-scale)); }
#dock .tray { display: flex; gap: 8px; overflow-x: auto; padding: 2px 12px 3px; scroll-snap-type: x proximity; scroll-padding-inline: 12px; }
#dock .pill { align-self: center; margin: 6px 0 8px; }
#dock[data-height="slim"] .tray, #dock[data-height="slim"] .pill { display: none; }
#dock[data-height="tray"] .pill { display: none; }
#dock[data-height="tucked"] .tray, #dock[data-height="tucked"] .cats, #dock[data-height="tucked"] .handle { display: none; }

/* Building cards, shared by the dock tray, the side panel grid and the rail drawer. */
.card { flex: 0 0 auto; width: 124px; min-height: 84px; scroll-snap-align: start; display: flex; flex-direction: column; align-items: flex-start; justify-content: flex-start; gap: 4px; padding: 8px; text-align: left; white-space: normal; }
.card .name { font-weight: 600; }
.card .cost { font-size: calc(12px * var(--ui-scale)); color: var(--ink-soft); }
.card.unaffordable .cost { color: var(--coral-text); }
/* A locked card fades only its picture: its name and its lock line keep full strength (4.5:1, spec 6.1) and say it in words. */
.card.locked .thumb { opacity: 0.6; filter: saturate(0.5); }
.cards.grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 8px; padding: 8px; grid-auto-rows: max-content; align-content: start; }
.cards.grid .card { width: auto; }
.cards .empty { padding: 12px; color: var(--ink-soft); }

/* Confirm bar for placing, drawing and demolishing. It never tucks away (spec 6.4). */
.confirm { display: flex; align-items: center; gap: 8px; box-sizing: border-box; padding: 8px 12px calc(8px + var(--safe-bottom)); background: var(--parchment); border-top: 1px solid var(--parchment-3); border-radius: 16px 16px 0 0; }   /* border-box: confirmBar.js holds its measured height as a min-height */
.confirm .info { flex: 1; min-width: 0; }
.confirm .info .title { font-weight: 600; }
.confirm .info .reason { font-size: calc(12px * var(--ui-scale)); }
.confirm.demolish { background: var(--coral-soft); }
body[data-layout="wide"] .confirm, body[data-layout="compact"] .confirm { padding-bottom: 8px; border-radius: 0; }

/* Wide layout: the right-hand panel. */
#panel { position: fixed; top: calc(var(--safe-top) + var(--topbar-h)); right: 0; bottom: 0; width: var(--panel-w); background: var(--parchment); border-left: 1px solid var(--parchment-3); display: flex; flex-direction: column; overflow: hidden; }
#panel .foldbar { display: flex; justify-content: flex-start; padding: 6px; border-bottom: 1px solid var(--parchment-3); }
#panel .minimap { align-self: center; margin: 8px; }
#panel .tabs { display: flex; flex-wrap: wrap; gap: 4px; padding: 0 8px 8px; }
#panel .tab { min-height: var(--tap); min-width: 0; padding: 0 8px; font-size: calc(12px * var(--ui-scale)); }   /* 8 px: five tabs fit one row at 100% text */
#panel .tab[aria-pressed="true"] { background: var(--teal); color: var(--teal-ink); box-shadow: none; }
#panel .cards { overflow-y: auto; flex: 1; }
#panel .details { border-top: 1px solid var(--parchment-3); padding: 8px 12px; min-height: 64px; }
body.panel-folded #panel { width: 56px; }
body.panel-folded #panel > :not(.foldbar) { display: none; }

/* Compact landscape: a slim rail on the right, trays as a drawer beside it. */
#rail { position: fixed; top: calc(var(--safe-top) + var(--topbar-h)); right: 0; bottom: 0; width: calc(var(--rail-w) + var(--safe-right)); padding: 6px var(--safe-right) calc(6px + var(--safe-bottom)) 0; background: var(--parchment); border-left: 1px solid var(--parchment-3); display: flex; flex-direction: column; align-items: center; gap: 4px; overflow-y: auto; }
#rail .cat { min-width: var(--tap); padding: 0; font-size: calc(18px * var(--ui-scale)); }
#rail .drawer { position: fixed; top: calc(var(--safe-top) + var(--topbar-h)); right: calc(var(--rail-w) + var(--safe-right)); bottom: 0; width: var(--drawer-w); background: var(--parchment-2); border-left: 1px solid var(--parchment-3); overflow-y: auto; }

/* Sheets: a bottom sheet in portrait, a drawer in compact, the lower part of the panel column in wide. */
#sheet { position: fixed; background: var(--parchment); box-shadow: var(--shadow); overflow-y: auto; display: flex; flex-direction: column; }
#sheet .head { display: flex; align-items: center; gap: 8px; padding: 8px 8px 8px 16px; position: sticky; top: 0; z-index: 1; background: var(--parchment); border-bottom: 1px solid var(--parchment-3); }   /* z-index: above content that scrolls under it, a disabled button's own layer (its opacity) too */
#sheet .head h2 { flex: 1; font-size: calc(18px * var(--ui-scale)); }
#sheet .body { padding: 8px 16px calc(16px + var(--safe-bottom)); }
#sheet .welcome-actions { position: sticky; bottom: 0; background: var(--parchment); border-top: 1px solid var(--parchment-3); margin: 12px -16px calc(-16px - var(--safe-bottom)); padding: 10px 16px calc(10px + var(--safe-bottom)); }   /* Carry on stays in view however many alerts there are */
#sheet ul { padding-left: 20px; margin: 6px 0; }
#sheet .tick { color: var(--teal); }
#sheet .cross { color: var(--coral-text); }
body[data-layout="portrait"] #sheet { left: 0; right: 0; bottom: 0; max-height: 60vh; max-height: 60dvh; border-radius: 16px 16px 0 0; }
body[data-layout="compact"] #sheet { top: calc(var(--safe-top) + var(--topbar-h)); right: calc(var(--rail-w) + var(--safe-right)); bottom: 0; width: var(--drawer-w); }
body[data-layout="wide"] #sheet { right: 0; bottom: 0; width: var(--panel-w); max-height: min(55%, calc(100% - var(--panel-tabs-bottom, 0px))); border-top: 1px solid var(--parchment-3); border-left: 1px solid var(--parchment-3); }   /* never over the side panel's tabs, however many rows they take (sidePanel.js publishes their bottom edge) */

/* Which chrome each layout shows. */
body[data-layout="portrait"] #panel, body[data-layout="portrait"] #rail { display: none; }
body[data-layout="wide"] #dock, body[data-layout="wide"] #rail, body[data-layout="wide"] #corners { display: none; }
body[data-layout="compact"] #dock, body[data-layout="compact"] #panel { display: none; }
body[data-layout="compact"] #corners { right: calc(var(--rail-w) + var(--safe-right)); bottom: calc(var(--safe-bottom) + 8px); }

/* Toasts and the confirm dialog. A toast sits above the corner buttons (spec 6.3): 8 px gap + the 52 px row + 4 px at 100% text, and the
   row grows about 20 px per whole step of text size (the Speed label, ruling 174), so the lift grows with --ui-scale. */
body { --toast-lift: calc(64px + 20px * (var(--ui-scale) - 1)); }
#toasts { position: fixed; left: 50%; transform: translateX(-50%); bottom: calc(var(--safe-bottom) + var(--dock-slim) + var(--toast-lift)); display: flex; flex-direction: column; align-items: center; gap: 6px; }
body[data-dock="tray"] #toasts { bottom: calc(var(--safe-bottom) + var(--dock-tray) + var(--toast-lift)); }
body[data-dock="tucked"] #toasts { bottom: calc(var(--safe-bottom) + var(--dock-tucked) + var(--toast-lift)); }
/* After a touch tick at large text the toast can drop into the free map between the corner buttons and the tray, so the new
   building fits above the corner buttons (createUI leavePlan); --toast-low is its bottom, set while that toast shows. */
body[data-layout="portrait"].toast-low #toasts { bottom: var(--toast-low); }
body[data-layout="wide"] #toasts { bottom: 24px; left: calc(50% - var(--panel-w) / 2); }
body[data-layout="compact"] #toasts { bottom: calc(var(--safe-bottom) + var(--toast-lift)); }   /* the compact corners sit on the home-bar inset too */
/* On a phone a toast may use the screen's width, up to 420 px, with 16 px at each side (ruling 169); the old half-screen box made
   it wrap a word a line at larger text. The sides run to the screen edge, or in compact to the rail; the same sets as the corners
   keep it clear of the card drawer, the confirm bar and a sheet. margin: auto centres the toast box when 420 px is narrower. */
body[data-layout="portrait"] #toasts, body[data-layout="compact"] #toasts { left: calc(var(--safe-left) + 16px); right: calc(var(--safe-right) + 16px); max-width: 420px; margin: 0 auto; transform: none; }
body[data-layout="compact"] #toasts { right: calc(var(--rail-w) + var(--safe-right) + 16px); }
body[data-layout="compact"]:has(#confirm:not([hidden])) #toasts, body[data-layout="compact"]:has(#sheet:not([hidden])) #toasts { right: calc(var(--rail-w) + var(--drawer-w) + var(--safe-right) + 16px); }
body[data-layout="compact"]:has(#rail .drawer:not([hidden])) #toasts { right: calc(var(--rail-w) + var(--drawer-w) + 17px + var(--safe-right) + 16px); }
body[data-layout="portrait"] .toast, body[data-layout="compact"] .toast { max-width: 100%; box-sizing: border-box; }
/* The phone container is as wide as the screen allows, far wider than a short toast: only the toast takes taps, so the map
   beside it still does (the column is otherwise an invisible strip over the tiles the player is placing on). */
body:is([data-layout="portrait"], [data-layout="compact"]) #toasts { pointer-events: none; }
body:is([data-layout="portrait"], [data-layout="compact"]) .toast { pointer-events: auto; }
.toast { background: var(--ink); color: var(--parchment); border-radius: var(--radius); padding: 6px 8px 6px 14px; display: flex; gap: 12px; align-items: center; box-shadow: var(--shadow); }
.toast button { min-height: var(--tap); background: transparent; color: var(--gold); border: none; font-weight: 600; }
.toast button:focus-visible { outline-color: var(--gold); }
#dialog { position: fixed; inset: 0; background: rgba(43, 31, 14, 0.45); display: flex; align-items: center; justify-content: center; }
#dialog .box { background: var(--parchment); border-radius: var(--radius); padding: 16px; max-width: 360px; width: calc(100% - 32px); box-shadow: var(--shadow); }
#dialog .buttons { display: flex; flex-wrap: wrap; gap: 8px; justify-content: flex-end; margin-top: 12px; }
/* Motion (spec 5.4, ruling 247): the interface follows the Motion setting, not the device. settings.js writes data-motion
   on the root: under Gentle and Still the chrome's transitions and animations are instant; Full keeps them even on a device
   that asks for less motion. Only before the settings apply (no data-motion yet) does the device's request decide. */
[data-motion="gentle"] *, [data-motion="still"] * { transition: none !important; animation: none !important; }
@media (prefers-reduced-motion: reduce) { body:not([data-motion]) * { transition: none !important; animation: none !important; } }
/* The confirm bar replaces the dock while a tool is active and never tucks (spec 6.4). */
#confirm { position: fixed; z-index: 3; }
body[data-layout="portrait"] #confirm { left: 0; right: 0; bottom: 0; }
body[data-layout="wide"] #confirm { right: 0; bottom: 0; width: var(--panel-w); }
body[data-layout="compact"] #confirm { right: calc(var(--rail-w) + var(--safe-right)); bottom: 0; width: var(--drawer-w); }
body[data-mode="placing"] #dock, body[data-mode="drawing"] #dock, body[data-mode="demolishing"] #dock { display: none; }
/* While a tool is on, the confirm bar covers the dock, so a toast (Well placed, Undo) sits above the bar and the corner row on it, not
   out in the middle of the map above a tray that is hidden behind the bar. The compact toast already rides the bar's column. */
body[data-layout="portrait"]:is([data-mode="placing"], [data-mode="drawing"], [data-mode="demolishing"]) #toasts { bottom: calc(var(--confirm-h, calc(var(--safe-bottom) + 96px)) + var(--toast-lift)); }
/* The corners sit on the confirm bar by its measured height (--confirm-h, set by confirmBar.js), whatever the text size. */
body[data-layout="portrait"][data-mode="placing"] #corners, body[data-layout="portrait"][data-mode="drawing"] #corners, body[data-layout="portrait"][data-mode="demolishing"] #corners { bottom: calc(var(--confirm-h, calc(var(--safe-bottom) + 96px)) + 8px); }
.speed-corner { position: relative; }
.speed-options { position: absolute; bottom: calc(100% + 6px); left: 0; display: flex; gap: 6px; }
/* Legend keys and the banner's warning glyph. */
#legend .key { display: inline-flex; align-items: center; gap: 4px; }
#banner .critical .text { flex: 1; min-width: 0; }
#banner .critical .icon { color: var(--coral-text); }
.toast .text { flex: 1; }

/* Menu, settings and help sheets. */
.menu-list { display: flex; flex-direction: column; gap: 8px; padding-top: 8px; }
.menu-list button { text-align: left; }
.switch { width: 100%; text-align: left; display: flex; justify-content: space-between; align-items: center; }
.switch[aria-checked="true"] { background: var(--teal-soft); border-color: var(--teal); }
.choices { display: flex; gap: 4px; flex-wrap: wrap; }
.choice { min-height: var(--tap); padding: 0 10px; font-size: calc(14px * var(--ui-scale)); }
.choice[aria-pressed="true"] { background: var(--teal); color: var(--teal-ink); box-shadow: none; }
.shortcuts { border-collapse: collapse; width: 100%; }
.shortcuts td, .shortcuts th { text-align: left; padding: 4px 8px 4px 0; border-bottom: 1px solid var(--parchment-2); vertical-align: top; }
kbd { font: inherit; background: var(--parchment-2); border-radius: 6px; padding: 0 6px; white-space: nowrap; }

/* The goals strip is the last row of #under-bar, below the banner and the legend when they show. */
#goals { position: static; display: flex; max-width: 100%; pointer-events: none; }
#goals > * { pointer-events: auto; }
/* The strip wraps by rows: the goal (label, words, chip, reward) keeps the width it needs, and the buttons drop to a
   row of their own when both do not fit. Only when the goal alone is wider than the strip do its words wrap, at spaces. */
.goal-strip { display: inline-flex; flex-wrap: wrap; align-items: center; column-gap: 8px; row-gap: 0; max-width: 100%; box-sizing: border-box; background: var(--parchment); border-radius: var(--radius); padding: 2px 4px 2px 12px; box-shadow: var(--shadow); }
.goal-strip .main { display: flex; align-items: center; gap: 8px; flex: 1 1 auto; min-width: 0; }
.goal-strip .text { flex: 0 1 auto; min-width: 0; }   /* the goal's words wrap rather than cut off, so a phone at large text still reads the whole goal */
.goal-strip .reward { flex: none; white-space: nowrap; }
.goal-strip .buttons { display: flex; flex-wrap: wrap; justify-content: flex-end; align-items: center; column-gap: 4px; flex: 0 1 auto; min-width: 0; margin-left: auto; }   /* on the narrowest phones at large text the third button drops to a row of its own */
.chip { flex: none; display: inline-block; background: var(--teal-soft); color: var(--ink); border-radius: 10px; padding: 0 8px; white-space: nowrap; }
body[data-layout="wide"] #goals { max-width: 560px; }
body[data-layout="compact"] #under-bar { right: calc(var(--rail-w) + var(--safe-right) + 8px); }
.goals { list-style: none; padding: 0; }
.goals .goal, .goals .daily { display: flex; align-items: center; justify-content: space-between; gap: 8px; padding: 6px 0; border-bottom: 1px solid var(--parchment-2); }
.goals button { flex: none; white-space: nowrap; }   /* Show me and Swap keep their words on one line; the goal's own words wrap beside them */
.goals .reward { white-space: nowrap; }                /* '+25 gold' stays together */
#sheet .goals + h3 { margin-top: 10px; }

/* The title card (spec 4.1) over the drifting sample city. While it shows, the game chrome stays hidden. */
#title { position: fixed; inset: 0; display: flex; align-items: center; justify-content: center; padding: 16px; background: rgba(43, 31, 14, 0.25); }
.title-card { background: var(--parchment); border-radius: 16px; box-shadow: var(--shadow); padding: 24px; width: min(360px, 100%); display: flex; flex-direction: column; gap: 8px; text-align: center; box-sizing: border-box; }
.title-card h1 { margin: 0; font-size: calc(32px * var(--ui-scale)); letter-spacing: 0.08em; }
.title-card .tagline { margin: 0 0 8px; }
.title-card .message { margin: 0 0 8px; }
.title-buttons { display: flex; flex-direction: column; gap: 8px; }
/* The in-app browser notice (spec 4.17): persistent, the first row of #under-bar so the top bar stays usable. */
#notice { position: static; display: flex; max-width: 100%; pointer-events: none; }
#notice > * { pointer-events: auto; }
#notice .notice { display: inline-flex; align-items: center; gap: 8px; background: var(--amber-soft); color: var(--amber-text); border: 1px solid var(--amber-text); border-radius: var(--radius); padding: 8px 12px; box-shadow: var(--shadow); }
body.on-title #ui > :not(#title):not(#under-bar) { display: none !important; }
body.on-title #under-bar > :not(#notice) { display: none !important; }
.guide { text-align: left; }
.guide .steps { padding-left: 20px; margin: 8px 0; }

/* Placement that explains (early-game pass 2.6). Compact: the bar is a narrow column beside the rail, so its words
   get a row of their own above the buttons and never wrap one word per line. */
body[data-layout="compact"] .confirm { flex-wrap: wrap; justify-content: flex-end; }
body[data-layout="compact"] .confirm .info { flex: 1 0 100%; }
/* Portrait: the bar keeps one height while the ghost moves. The corner buttons sit on its measured height, so an info block
   that grew a line when a warning, a reason or an effect appeared would make them jump. 3.5em is three text lines:
   the title (1.4em) plus the cost and the reason or effect (12px at the body size is .75em, so 1.05em each). */
body[data-layout="portrait"] .confirm .info { min-height: 3.5em; }
/* At 125% and 150% text the words column is only about 150px wide, so a reason or an effect wraps to two lines: keep both (4.55em). */
body[data-layout="portrait"][data-text-size="125"] .confirm .info, body[data-layout="portrait"][data-text-size="150"] .confirm .info { min-height: 4.55em; }

/* Conditions in the confirm bar (placement review P9): a row of chips under the words and the buttons, full width. Each chip has its
   mark and its words (met teal, later amber, can't coral), 14 px at the body size and at least 24 px tall. The row is there for
   every building tool, so the bar does not jump as the ghost moves; a terrain building's chips can run to a second row ("Fertile
   land: 2 of 4 tiles"), so its row keeps room for two from the start (three on a bar 365 px wide or less at 150% text). */
.confirm.has-conds { flex-wrap: wrap; row-gap: 6px; }
.confirm .conds { --chip-h: max(24px, calc(14px * var(--ui-scale) * 1.2 + 4px)); flex: 1 0 100%; order: 9; display: flex; flex-wrap: wrap; align-content: flex-start; gap: 4px 6px; min-height: var(--chip-h); }
.confirm .conds.tall { min-height: calc(2 * var(--chip-h) + 4px); }
body[data-text-size="150"] .confirm .conds.tall.narrow { min-height: calc(3 * var(--chip-h) + 8px); }   /* a 360 px phone at 150% text fits one chip a row at worst (confirmBar.js) */
.cond { display: inline-flex; align-items: center; gap: 4px; box-sizing: border-box; min-height: 24px; padding: 1px 8px; border: 1px solid transparent; border-radius: 12px; font-size: calc(14px * var(--ui-scale)); line-height: 1.2; }
.cond .mark { font-weight: 700; }
.cond.met { background: var(--teal-soft); color: var(--ink); border-color: var(--teal); }
.cond.later { background: var(--amber-soft); color: var(--amber-text); border-color: var(--amber-text); }
.cond.cant { background: var(--coral-soft); color: var(--coral-text); border-color: var(--coral-text); }
.card .terrain { margin-top: -3px; font-size: calc(12px * var(--ui-scale)); line-height: 1.15; color: var(--ink-soft); }   /* it hugs the cost line, so a farm's card stays inside --dock-tray */

/* Cards with pictures (early-game pass 2.5): the building's drawing over its name and cost; the coin is decoration. */
/* The picture is drawn at its own size (THUMB_W x THUMB_H in src/ui/thumbs.js); where a scrollbar narrows a card it shrinks, never stretches. */
.card .thumb { display: block; align-self: center; width: 104px; max-width: 100%; height: 52px; object-fit: contain; flex: 0 0 auto; }
.card .coin { display: inline-block; width: 0.75em; height: 0.75em; margin-right: 4px; border-radius: 50%; background: var(--gold); box-shadow: inset 0 0 0 1px #B98A4A; vertical-align: -0.05em; }
/* Category bands: a thin stripe in the category's emblem colour (--tint, set on each button). A background image, so
   it survives the dock's background: none and the pressed tab's teal fill; it adds no height. The teal fill would
   swallow most tints, so the pressed tab's band turns white. */
#dock .cat, #rail .cat, #panel .tab, #panel .tab[aria-pressed="true"] { background-image: linear-gradient(var(--tint, transparent), var(--tint, transparent)); background-repeat: no-repeat; background-position: 50% 100%; background-size: 60% 3px; }
#panel .tab[aria-pressed="true"] { background-image: linear-gradient(var(--teal-ink), var(--teal-ink)); }
/* The rail's pressed button wears a 2 px teal ring inside its edge; lift the band clear of it so it still shows. */
#rail .cat { background-position: 50% calc(100% - 4px); }

/* Corner buttons with words (early-game pass 2.9): an icon over a short word; the speed button reads ▶ Play or ❚❚ 2×. */
#corners .labelled { display: inline-flex; flex-direction: column; align-items: center; justify-content: center; min-width: 48px; min-height: 52px; padding: 2px 8px; line-height: 1.1; }
#corners .labelled .label { font-size: calc(12px * var(--ui-scale)); white-space: nowrap; }
#corners .speed-main, #corners .speed-chevron { min-height: 52px; }
.speed-main .icon { width: auto; margin-right: 6px; }
/* The face and hidden copies of every state share one grid cell, so the button keeps the width of its widest state. */
#corners .speed-main { display: inline-grid; align-items: center; justify-items: center; }
#corners .speed-main > .face, #corners .speed-main > .sizer { grid-area: 1 / 1; white-space: nowrap; }
#corners .speed-main > .sizer { visibility: hidden; }
#corners .speed-chevron .icon { font-size: 1.5em; line-height: 1; }   /* ▴ is a small glyph: let it read as a chevron */
/* Compact: the confirm bar, sheets and the card drawer share the column beside the rail, so the corners step left of it
   (they are never open together). The drawer is the card grid's 8 px padding either side plus its 1 px border wider than --drawer-w. */
body[data-layout="compact"]:has(#confirm:not([hidden])) #corners,
body[data-layout="compact"]:has(#sheet:not([hidden])) #corners { right: calc(var(--rail-w) + var(--drawer-w) + var(--safe-right)); }
body[data-layout="compact"]:has(#rail .drawer:not([hidden])) #corners { right: calc(var(--rail-w) + var(--drawer-w) + 17px + var(--safe-right)); }
/* A phone held sideways under 700 px wide (body.narrow, set by layout.js) at 125% or 150% text has too little map beside
   the drawer for both corner groups with their words: Map and Layers keep their icons and drop the word from sight. Each
   button's accessible name still starts with its word. */
body.narrow[data-layout="compact"]:is([data-text-size="125"], [data-text-size="150"]) #corners .labelled .label { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; }

/* The house sheet (early-game pass 2.10): each need on its own row with a mark, its name and a status in
   words; a missing one adds its cause, its fix and Show me. Residents carry a small meter. */
/* --mark-w is the tick or cross column at body size (1.25 em of 16 px): the cause and the fix indent by it and the
   8 px gap, so they line up under the need's name whatever their own (smaller) font size. */
#sheet .checklist { list-style: none; padding-left: 0; --mark-w: calc(20px * var(--ui-scale)); }
.checklist li { padding: 6px 0; border-bottom: 1px solid var(--parchment-2); }
.checklist .need { display: flex; align-items: center; gap: 8px; }
.checklist .need .name { flex: 1; min-width: 0; }
.checklist .tick, .checklist .cross { display: inline-block; width: var(--mark-w); text-align: center; font-weight: 700; }
.checklist .cause { margin: 2px 0 0 calc(var(--mark-w) + 8px); }
.checklist .fix { display: flex; align-items: center; justify-content: space-between; flex-wrap: wrap; gap: 4px 8px; margin: 4px 0 0 calc(var(--mark-w) + 8px); }
.row.residents .value { display: inline-flex; align-items: center; gap: 8px; }
.meter { display: inline-block; width: 64px; height: 8px; border-radius: 4px; background: var(--parchment-2); box-shadow: inset 0 0 0 1px var(--parchment-3); overflow: hidden; }
.meter .fill { display: block; height: 100%; background: var(--teal); }

/* The guided start (early-game pass 3.2): the suggested card has a teal ring and a tag; the other cards keep
   their words at full strength (4.5:1) and only their picture fades; the suggested category carries a dot. */
.card, #dock .cat, #rail .cat, #panel .tab { position: relative; }
.card.suggested { border-color: var(--teal); box-shadow: inset 0 0 0 2px var(--teal); }
.card .tag { position: absolute; top: 4px; right: 4px; background: var(--teal); color: var(--teal-ink); border-radius: 8px; padding: 0 6px; font-size: calc(12px * var(--ui-scale)); line-height: 1.6; font-weight: 600; pointer-events: none; }
.card.dimmed { border-color: transparent; }
.card.dimmed .thumb { opacity: 0.6; filter: saturate(0.5); }
.cat.dimmed .icon { opacity: 0.6; }
.cat.suggested::after, .tab.suggested::after { content: ""; position: absolute; top: 4px; right: 4px; width: 8px; height: 8px; border-radius: 50%; background: var(--teal); box-shadow: 0 0 0 2px var(--parchment); }
/* On the pressed tab (a teal fill) the dot turns white, so it still reads as a filled dot. */
#panel .tab[aria-pressed="true"].suggested::after { background: var(--teal-ink); box-shadow: 0 0 0 2px var(--teal); }

/* Celebration captions (early-game pass 3.6): the last row of #under-bar, a parchment card with a gold star. Nothing to
   tap, so the row lets taps through to the map. The head (bold) and the gold sit on the first line and the why beneath it,
   smaller. Text is ink on parchment, the why ink-soft (7:1) and the gold teal (4.8:1). */
#cheer { position: static; display: flex; max-width: 100%; pointer-events: none; }
/* #cheer is a live region, so it stays rendered while empty (a region that appears together with its words can go
   unannounced): collapsed to a clipped dot that is out of the flow, so it leaves no gap in #under-bar. */
#under-bar > #cheer:empty { display: block; position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); }
#cheer .cheer { position: relative; display: inline-grid; grid-template-columns: auto minmax(0, 1fr) auto; align-items: baseline; max-width: 100%; box-sizing: border-box; background: var(--parchment); color: var(--ink); border: 1px solid var(--gold); border-radius: var(--radius); padding: 6px 12px; box-shadow: var(--shadow); animation: cheer-in 200ms ease-out; }
#cheer .cheer .icon { grid-column: 1; grid-row: 1; margin-right: 8px; color: var(--amber-text); }
#cheer .cheer .text { grid-column: 2; grid-row: 1; min-width: 0; font-weight: 600; }
#cheer .cheer .reward { grid-column: 3; grid-row: 1; margin-left: 8px; color: var(--teal); font-weight: 600; white-space: nowrap; }
#cheer .cheer .why { grid-column: 2 / 4; grid-row: 2; font-size: calc(14px * var(--ui-scale)); line-height: 1.35; color: var(--ink-soft); }
/* A phone at larger text keeps the caption to the head and the gold, so it stays clear of the middle of the map; the
   why stays in the live region for screen readers. There the caption is one flow of words (the star, the head, then
   the gold), so it wraps as a sentence in two lines at most rather than a column beside the gold. */
body:is([data-layout="portrait"], [data-layout="compact"]):is([data-text-size="125"], [data-text-size="150"]) #cheer .cheer .why { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; }
body:is([data-layout="portrait"], [data-layout="compact"]):is([data-text-size="125"], [data-text-size="150"]) #cheer .cheer { display: block; padding: 4px 10px; font-size: calc(14px * var(--ui-scale)); line-height: 1.25; }
body:is([data-layout="portrait"], [data-layout="compact"]):is([data-text-size="125"], [data-text-size="150"]) #cheer .cheer .icon { margin-right: 4px; }
body:is([data-layout="portrait"], [data-layout="compact"]):is([data-text-size="125"], [data-text-size="150"]) #cheer .cheer .reward { margin-left: 6px; }
#cheer .cheer.milestone { background: var(--amber-soft); border-color: var(--amber-text); }
body[data-layout="wide"] #cheer { max-width: 440px; }
@keyframes cheer-in { from { opacity: 0; transform: translateY(-4px); } to { opacity: 1; transform: none; } }
body.reduce-motion #cheer .cheer { animation: none; }

/* The tip row (early-game pass 3.4): under the goals strip, one tip at a time with Got it. Ink on teal-soft (6.7:1 for the
   muted label). Like #cheer, #tip is a live region that stays rendered while empty (collapsed to a clipped dot that is
   out of the flow, so it leaves no gap), so a screen reader already has it when a tip arrives. */
#tip { position: static; display: flex; max-width: 100%; pointer-events: none; }
#tip > * { pointer-events: auto; }
#under-bar > #tip:empty { display: block; position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); }
#tip .tip-row { display: inline-flex; align-items: center; gap: 8px; max-width: 100%; box-sizing: border-box; background: var(--teal-soft); color: var(--ink); border: 1px solid var(--teal); border-radius: var(--radius); padding: 2px 4px 2px 12px; box-shadow: var(--shadow); }
#tip .tip-row .text { min-width: 0; }
#tip .tip-row .body { position: relative; display: flex; align-items: center; gap: inherit; min-width: 0; }   /* the label and the words: the one place a tap opens a cut tip, and the box "More ▾" sits in */
#tip .tip-row .label, #tip .tip-row .got-it { flex: none; }
#tip .tip-row .got-it { white-space: nowrap; }
body[data-layout="wide"] #tip { max-width: 440px; }
/* A phone (portrait or compact) at 125% or 150% text has no room for a tip to stay up and cover the map: its words are 14 px
   scaled instead of the body size, its padding is tight, and the Tip label is kept for screen readers only, as the caption's
   why is. On every phone, at any text size, createUI also closes a tip after TIP_AUTO_CLOSE_MS of unpaused play with it on
   screen (ruling 171; it stays seen, and How to play lists it). */
body:is([data-layout="portrait"], [data-layout="compact"]):is([data-text-size="125"], [data-text-size="150"]) #tip .tip-row { position: relative; padding: 0 2px 0 10px; gap: 4px; line-height: 1.25; }
body:is([data-layout="portrait"], [data-layout="compact"]):is([data-text-size="125"], [data-text-size="150"]) #tip .tip-row .text { font-size: calc(14px * var(--ui-scale)); }
body:is([data-layout="portrait"], [data-layout="compact"]):is([data-text-size="125"], [data-text-size="150"]) #tip .tip-row .label { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; }
body:is([data-layout="portrait"], [data-layout="compact"]):is([data-text-size="125"], [data-text-size="150"]) #tip .tip-row .got-it { padding: 0 6px; font-size: calc(14px * var(--ui-scale)); }

/* The guidance row on a phone (spec 6.3, rulings 163 and 164): under the alert row one of the overlay legend, a tip, a
   caption or the goal shows, the one createUI names in data-guidance ('none' while a sheet is open, and on a landscape
   phone at 125% text or more while a banner is up, ruling 172: its one row is the alert row). #tip and #cheer are
   live regions that stay rendered: their contents hide instead. The goals strip is one row (ruling 170): the label, the
   words and the progress are one flow of words on one button (two lines at most, cut with "…"; the button's name has all
   of it), then Show me. A goal's caption drops its "Goal done:" lead from sight (screen readers keep it) and reads as the strip
   changing: "✓ Lay a road +25 gold". */
body:is([data-layout="portrait"], [data-layout="compact"]):not([data-guidance="legend"]) #legend,
body:is([data-layout="portrait"], [data-layout="compact"]):not([data-guidance="goal"]) #goals { display: none !important; }
body:is([data-layout="portrait"], [data-layout="compact"]):not([data-guidance="tip"]) #tip > *,
body:is([data-layout="portrait"], [data-layout="compact"]):not([data-guidance="caption"]) #cheer > * { display: none !important; }
.goal-strip.one-row { flex-wrap: nowrap; column-gap: 4px; padding: 0 4px 0 0; }
.goal-strip.one-row .main { position: relative; display: flex; align-items: center; flex: 1 1 auto; min-width: 0; min-height: var(--tap); padding: 2px 4px 2px 12px; background: transparent; border-color: transparent; text-align: left; line-height: 1.25; }
.goal-strip.one-row .words { display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 2; line-clamp: 2; overflow: hidden; overflow-wrap: anywhere; min-width: 0; }
.goal-strip.one-row .buttons { flex: none; flex-wrap: nowrap; margin-left: 0; }
body:is([data-layout="portrait"], [data-layout="compact"]) #cheer .cheer.led .lead { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; }
/* A caption in the guidance row is two lines at most: at 100% text the head (with the gold beside it) takes two at most
   and the why one beneath; at larger text the whole caption is one flow of two lines. Past that it is cut with "…": the
   live region reads it whole, and the Goals sheet's Recent list has it in full. */
body:is([data-layout="portrait"], [data-layout="compact"]) #cheer .cheer .text { display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 2; line-clamp: 2; overflow: hidden; }
body:is([data-layout="portrait"], [data-layout="compact"]) #cheer .cheer .why { display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 1; line-clamp: 1; overflow: hidden; }
body:is([data-layout="portrait"], [data-layout="compact"]):is([data-text-size="125"], [data-text-size="150"]) #cheer .cheer { display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 2; line-clamp: 2; overflow: hidden; }
body:is([data-layout="portrait"], [data-layout="compact"]):is([data-text-size="125"], [data-text-size="150"]) #cheer .cheer .text { display: inline; }
#sheet .recent .why { display: block; }
/* A tip in the guidance row is two lines at most too; How to play lists every tip in full, and the live region reads it whole. */
body:is([data-layout="portrait"], [data-layout="compact"]) #tip .tip-row .text { display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 2; line-clamp: 2; overflow: hidden; }
/* The guidance row opened in place (placement review P8, spec 6.3). A phone cuts a goal, a tip or a caption at two lines;
   while the words are cut, "More ▾" sits over the end of the last line on a fade of the row's own colour (expand.js writes
   it, createUI shows it only when the words are measured as cut). A tap on the row opens it: every clamp above comes off, the
   row grows down to --guidance-max (createUI: 40% of the screen's height less where the row starts, spec 6.1) and scrolls
   inside beyond it. It eases open in 150 ms, at once under reduced motion. "Less ▴" closes it, as do Back, Esc and a tap on
   the map. Teal on parchment and amber is 4.8:1; on the tip's teal-soft it is ink. */
#goals .goal-strip, #cheer .cheer { --row-bg: var(--parchment); }
#cheer .cheer.milestone { --row-bg: var(--amber-soft); }
#tip .tip-row { --row-bg: var(--teal-soft); }
.goal-strip .more, #tip .more, #cheer .more { position: absolute; right: 2px; bottom: 1px; min-width: 0; min-height: 0; margin: 0; padding: 0 2px 0 22px; border: none; border-radius: 0; box-shadow: none; background: linear-gradient(to right, transparent, var(--row-bg) 20px); color: var(--teal); font-size: calc(12px * var(--ui-scale)); font-weight: 600; line-height: 1.5; white-space: nowrap; }
#tip .more { color: var(--ink); }
#cheer .more { pointer-events: auto; }
#cheer .cheer:has(.more:not([hidden])) { pointer-events: auto; cursor: pointer; }   /* a cut caption can be tapped; any other still lets taps through to the map */
#tip .body:has(.more:not([hidden])) { cursor: pointer; }
/* The tip's and the caption's More and Less are real buttons, so each gets a 44 px target (spec 6.1, 6.8) without making the row
   taller: an invisible box at least 44 px tall and wide that ends 1 px under the word and reaches up over the cut words (which
   take the same tap) and never right, where Got it sits. */
#tip .more::before, #cheer .more::before { content: ''; position: absolute; right: 0; bottom: -1px; width: max(100%, var(--tap)); height: max(calc(100% + 1px), var(--tap)); }
/* At larger text a caption is one flow of head and gold, its why hidden from sight: "More ▾" has a place of its own at the end
   of the row, so it never sits over the head. */
body:is([data-layout="portrait"], [data-layout="compact"]):is([data-text-size="125"], [data-text-size="150"]) #cheer .cheer:not(.open):has(.more:not([hidden])) { padding-right: calc(28px + 46px * var(--ui-scale)); }
/* Open: the goal's words in full, its reward, then Show me, All goals and Skip guide on a row of their own. */
.goal-strip.one-row.open { display: flex; flex-wrap: wrap; align-self: stretch; max-height: var(--guidance-max, 40vh); overflow-y: auto; padding: 0 4px 4px 0; }
.goal-strip.one-row.open .main { flex: 1 1 100%; }
.goal-strip.one-row.open .words { display: block; overflow: visible; }
.goal-strip.one-row.open .buttons { flex: 1 1 100%; flex-wrap: wrap; justify-content: flex-start; padding-left: 8px; }
.goal-strip.one-row.open .more { position: static; align-self: flex-end; padding: 0 0 0 8px; background: none; }
.goal-strip.open ~ .threshold { display: none; }                  /* the next-threshold bar would push the rows past 40% */
/* Open: the tip's words in full, Got it still at the top right. */
#tip .tip-row.open { display: flex; align-items: flex-start; max-height: var(--guidance-max, 40vh); overflow-y: auto; }
#tip .tip-row.open .body { flex: 1 1 auto; flex-wrap: wrap; align-items: flex-start; }
#tip .tip-row.open .text { display: block !important; flex: 1 1 0; overflow: visible !important; }   /* !important: the phone clamps above are more specific */
#tip .tip-row.open .more { position: sticky; bottom: 0; flex: 1 1 100%; padding: 0 4px; text-align: right; background: var(--row-bg); box-shadow: 0 -8px 8px -2px var(--row-bg); }   /* sticky: Less stays in sight, on a fade that says the words scroll (P8) */
/* Open: the caption's head, its gold and its why in full, at every text size (the why returns to sight where larger text hid it),
   in the grid the caption has at 100%: the star, the head and the gold on the first row, the why beneath, Less at the end. At
   larger text the gold sits under the head, so the head keeps the width of the row. */
#cheer .cheer.open { display: grid !important; width: 100%; max-height: var(--guidance-max, 40vh); overflow: hidden auto !important; padding: 4px 12px; }   /* !important: the phone clamps above are more specific */
#cheer .cheer.open .text { display: block !important; overflow: visible !important; }
#cheer .cheer.open .why { position: static !important; display: block !important; width: auto !important; height: auto !important; clip: auto !important; overflow: visible !important; white-space: normal !important; }
#cheer .cheer.open .more { position: sticky; bottom: 0; grid-column: 2 / 4; grid-row: 4; justify-self: end; padding: 0 0 0 8px; background: var(--row-bg); box-shadow: 0 -8px 8px -2px var(--row-bg); }   /* last: row 4 stays empty-height where the why is row 2 */
body:is([data-layout="portrait"], [data-layout="compact"]):is([data-text-size="125"], [data-text-size="150"]) #cheer .cheer.open .reward { grid-column: 2; grid-row: 2; justify-self: start; margin-left: 0; }
body:is([data-layout="portrait"], [data-layout="compact"]):is([data-text-size="125"], [data-text-size="150"]) #cheer .cheer.open { grid-template-columns: auto minmax(0, 1fr); }
body:is([data-layout="portrait"], [data-layout="compact"]):is([data-text-size="125"], [data-text-size="150"]) #cheer .cheer.open .why { grid-column: 2; grid-row: 3; }
body:is([data-layout="portrait"], [data-layout="compact"]):is([data-text-size="125"], [data-text-size="150"]) #cheer .cheer.open .more { grid-column: 2; }
@keyframes guidance-open { from { clip-path: inset(0 0 55% 0); } to { clip-path: inset(0); } }
.goal-strip.one-row.open, #tip .tip-row.open, #cheer .cheer.open { animation: guidance-open 150ms ease-out; }
body.reduce-motion .goal-strip.one-row.open, body.reduce-motion #tip .tip-row.open, body.reduce-motion #cheer .cheer.open { animation: none; }
#sheet .skip-row { margin: 4px 0; }
#sheet .recent { list-style: none; padding: 0; margin: 4px 0; }
#sheet .recent li { display: flex; justify-content: space-between; align-items: baseline; gap: 8px; padding: 4px 0; border-bottom: 1px solid var(--parchment-2); }
#sheet .recent .reward { flex: none; white-space: nowrap; }

/* Landscape phones and large text (mobile UX review: ruling 172, section 4 items 8 and 9; spec 6.3 "Small phones").
   Held sideways the alert row and the guidance row end by 40% of the height at 100% text: they sit 4 px apart, and the
   banner and the legend keep 2 px above and below their 44 px buttons. At 125% and 150% only one row shows (createUI's
   guidance rule). The rows stop at the left edge of the column beside the rail while a sheet or the card drawer holds
   it, as the corner buttons do. */
body[data-layout="compact"] #under-bar { gap: 4px; }
body[data-layout="compact"] #banner .critical, body[data-layout="compact"] #legend .strip { padding-top: 2px; padding-bottom: 2px; }
body[data-layout="compact"]:has(#sheet:not([hidden])) #under-bar { right: calc(var(--rail-w) + var(--drawer-w) + var(--safe-right) + 8px); }
body[data-layout="compact"]:has(#rail .drawer:not([hidden])) #under-bar { right: calc(var(--rail-w) + var(--drawer-w) + 17px + var(--safe-right) + 8px); }
/* Held sideways the fire-risk legend keeps a plain strip's height at 100% text: its name and the four bands on one row,
   the two firehouse keys beneath them on one line, the x beside both rows; what never burns is kept for screen readers,
   as on a portrait phone (the building sheet's fire row says it for each building). */
body[data-layout="compact"] #legend .strip.graded:has(.fire-keys) { display: inline-grid; grid-template-columns: auto minmax(0, auto) auto; column-gap: 8px; row-gap: 0; align-items: center; }
body[data-layout="compact"] #legend .strip.graded:has(.fire-keys) > strong.text { grid-column: 1; grid-row: 1; }
body[data-layout="compact"] #legend .strip.graded .scale:has(+ .fire-keys) { grid-column: 2; grid-row: 1; justify-self: start; }
body[data-layout="compact"] #legend .strip.graded .fire-keys { grid-column: 1 / 3; grid-row: 2; column-gap: 8px; }
body[data-layout="compact"] #legend .strip.graded:has(.fire-keys) .close { grid-column: 3; grid-row: 1 / 3; }
body[data-layout="compact"] #legend .fire-keys .never-burn { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; }
/* The top bar on a phone at 150% text (section 4 item 8): each number wears its small picture (hud.js) in place of its
   word, which stays for screen readers, and the gaps tighten, so a real city's numbers keep to one row. */
#topbar .stat .glyph { display: none; }
body:is([data-layout="portrait"], [data-layout="compact"])[data-text-size="150"] #topbar .stat { gap: 2px; }
body:is([data-layout="portrait"], [data-layout="compact"])[data-text-size="150"] #topbar .stats { column-gap: 6px; }
body:is([data-layout="portrait"], [data-layout="compact"])[data-text-size="150"] #topbar .stat .glyph { display: inline-block; font-size: calc(12px * var(--ui-scale)); color: var(--ink-soft); }
body:is([data-layout="portrait"], [data-layout="compact"])[data-text-size="150"] #topbar .stat .glyph.coin { width: 0.8em; height: 0.8em; border-radius: 50%; background: var(--gold); box-shadow: inset 0 0 0 1px #B98A4A; }
body:is([data-layout="portrait"], [data-layout="compact"])[data-text-size="150"] #topbar .stat .label { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; }
/* The score chip (section 4 item 9) joins the numbers' flow, right after the date, on a landscape phone (where the bar's
   far end is under ☰ and the rail) and on a portrait phone at 150% text (where one row of numbers leaves it no corner):
   on the numbers' row where it fits, else at the start of the next. */
body[data-layout="compact"] #topbar .stats, body[data-layout="portrait"][data-text-size="150"] #topbar .stats { padding-bottom: 0; }
body[data-layout="compact"] #topbar .score-chip, body[data-layout="portrait"][data-text-size="150"] #topbar .score-chip { position: static; line-height: 1.2; }

/* The idle nudge (early-game pass 3.5): its one line sits under the goals strip, so #goals stacks in a column. Show me
   gets a gold ring that pulses twice (under 1 flash a second, done in 2.4 s), then stays still until the
   player acts; under reduced motion only the still ring shows. The Play tip rings the speed control the same way. */
#goals { flex-direction: column; align-items: flex-start; }
#goals .nudge { max-width: 100%; pointer-events: none; }   /* a polite live region that stays rendered: no height of its own while its line is hidden */
#goals .nudge-line { margin: 4px 0 0; max-width: 100%; box-sizing: border-box; background: var(--parchment); color: var(--ink); border-radius: var(--radius); padding: 4px 12px; box-shadow: var(--shadow); pointer-events: none; }
.goal-strip .show-me.glow { box-shadow: 0 0 0 3px var(--gold); animation: nudge-glow 1.2s ease-in-out 2; }
body.nudge-play .speed-main { box-shadow: 0 0 0 3px var(--gold); animation: nudge-glow 1.2s ease-in-out 2; }
body.nudge-play #topbar .speed-bar { border-radius: var(--radius); box-shadow: 0 0 0 3px var(--gold); animation: nudge-glow 1.2s ease-in-out 2; }
@keyframes nudge-glow { 0%, 100% { box-shadow: 0 0 0 3px var(--gold); } 50% { box-shadow: 0 0 0 8px rgba(226, 177, 60, 0.35); } }
body.reduce-motion .goal-strip .show-me.glow, body.reduce-motion.nudge-play .speed-main, body.reduce-motion.nudge-play #topbar .speed-bar { animation: none; }

/* Welcome back trends (early-game pass 3.9): an arrow column, then the words. Teal up, coral down, both beside words. */
#sheet .trends { list-style: none; padding-left: 0; }
.trends .trend-row { display: flex; align-items: baseline; gap: 6px; padding: 2px 0; }
.trends .trend { display: inline-block; width: 1.25em; text-align: center; }
.trends .trend.up { color: var(--teal); }
.trends .trend.down { color: var(--coral-text); }
.trends .trend-row:not(.up):not(.down) .words { margin-left: calc(1.25em + 6px); }   /* steady and first-time rows line up with the arrowed ones */
.goals.today .status { white-space: nowrap; }                                       /* "Done" and "2 of 6" keep to one line */
#sheet .closest { margin: 8px 0; }

/* Achievements at the end of the city summary (early-game pass 3.8): name and condition on the left, progress on the right. */
#sheet .achievements { list-style: none; padding-left: 0; }
.achievements .achievement { display: flex; justify-content: space-between; align-items: center; gap: 12px; padding: 6px 0; border-bottom: 1px solid var(--parchment-2); }
.achievements .what { display: flex; flex-direction: column; min-width: 0; }
.achievements .status { display: flex; align-items: center; gap: 6px; flex: none; white-space: nowrap; }
.ach-bar { display: inline-block; width: 64px; height: 8px; border-radius: 4px; background: var(--parchment); border: 1px solid var(--ink-soft); overflow: hidden; box-sizing: border-box; }
.ach-bar .fill { display: block; height: 100%; background: var(--teal); }

/* Stage 2 cards and storage (spec 4.10, 4.14). A card whose building a milestone just unlocked wears a "New" badge: a
   word, ink on gold (8:1), top left, clear of the guide's Suggested tag on the right. A producer's sheet leads with
   what it is doing; a storehouse lists its goods as switches whose state is a word beside the good. */
.card .new-badge { position: absolute; top: 4px; left: 4px; background: var(--gold); color: var(--ink); border-radius: 8px; padding: 0 6px; font-size: calc(12px * var(--ui-scale)); line-height: 1.6; font-weight: 600; pointer-events: none; }
#sheet .work-status { margin: 4px 0 8px; font-weight: 600; }
#sheet .store-goods { list-style: none; padding-left: 0; }
.store-goods li { padding: 4px 0; border-bottom: 1px solid var(--parchment-2); }
.store-goods .row { border-bottom: none; padding: 2px 0; }
.store-goods .switch { margin: 2px 0; gap: 8px; }
.store-goods .switch .state { font-weight: 600; flex: none; }
.store-goods .switch[aria-checked="false"] .state { color: var(--coral-text); }
.store-goods .good-line { margin: 2px 0 0; }

/* The trade sheet (spec 4.15): routes first, then a row per good with its prices, the Buy, Sell or Ignore choice
   (the .choices group the settings use) and the level to keep between its 44 px -10 and +10 buttons. */
#sheet .routes, #sheet .trade-goods { list-style: none; padding-left: 0; }
.routes .route { padding: 6px 0; border-bottom: 1px solid var(--parchment-2); }
.routes .route.open { display: flex; justify-content: space-between; align-items: center; min-height: var(--tap); }
.routes .route .row { border-bottom: none; align-items: center; padding: 0; }
.routes .reason { margin: 2px 0 0; }
.trade-goods .trade-good { padding: 6px 0; border-bottom: 1px solid var(--parchment-2); }
.trade-goods .row { border-bottom: none; padding: 0; align-items: baseline; }
.trade-goods .prices { margin: 2px 0 4px; }
.trade-goods .keep { display: flex; align-items: center; gap: 8px; margin-top: 6px; }
.trade-goods .keep-value { min-width: 5em; text-align: center; }
.trade-goods .keep-help { margin: 2px 0 0; }
.trade-warning { margin: 8px 0; }
.trade-warning p { margin: 0 0 4px; }
.trade-warning .fix { display: flex; align-items: center; justify-content: space-between; flex-wrap: wrap; gap: 4px 8px; }
#sheet .next-caravan { margin-top: 8px; }

/* The house sheet for the economy (spec 4.6, 4.7): the desirability row's score and causes sit under it, lined up with
   the cause and fix. */
.checklist .desirability { margin: 2px 0 0 calc(var(--mark-w) + 8px); }

/* Stage 2 overlays and the placement preview (spec 4.7, 6.3). The desirability legend is a scale: a swatch over a short
   label per band, the first hatched as on the map. On a phone held upright the scale takes a row of its own under the name
   and the x; held sideways (compact) it keeps one row, so the strip and the goals under it stay out of the middle. */
#legend .strip.graded { flex-wrap: wrap; row-gap: 0; max-width: 100%; box-sizing: border-box; }
#legend .scale { display: inline-flex; flex-wrap: wrap; align-items: flex-start; gap: 2px 6px; }
#legend .band { display: inline-flex; flex-direction: column; align-items: center; min-width: 32px; }
#legend .swatch { display: block; width: 26px; height: 12px; border-radius: 3px; box-shadow: inset 0 0 0 1px rgba(43, 31, 14, 0.35); }
#legend .band .text { line-height: 1.1; white-space: nowrap; }
body[data-layout="portrait"] #legend .strip.graded .scale { order: 3; flex-basis: 100%; }
body[data-layout="portrait"] #legend .strip.graded .close { margin-left: auto; }
/* The Ground overlay's legend (placement review P1). A portrait phone: the name, then two swatches with their words and, while a
   farm, orchard or mine is being placed, a third line for the brighter tiles, in one column with the x beside all three, so the
   third line costs no taller strip than the x's own 44 px. A landscape phone: one flex row (name, keys, x) whose keys wrap among
   themselves. At 125% text and more on either phone the brighter-tiles words move to screen readers only (sr-only): the budget
   (spec 6.3, rows end by 40% of the height) leaves no third line, and the map shows the bright tiles themselves. */
#legend .strip.ground { display: grid; grid-template-columns: minmax(0, 1fr) auto; align-items: center; column-gap: 10px; row-gap: 0; max-width: 100%; box-sizing: border-box; }
#legend .strip.ground > strong.text { grid-column: 1; grid-row: 1; }
#legend .strip.ground > .close { grid-column: 2; grid-row: 1 / 4; }
#legend .strip.ground .ground-keys { grid-column: 1; grid-row: 2; display: flex; flex-wrap: wrap; align-items: center; gap: 0 12px; padding-bottom: 2px; }
#legend .strip.ground .ground-keys .room { flex-basis: 100%; }
#legend .strip.ground .key { gap: 6px; }
body[data-layout="compact"] #legend .strip.ground { display: flex; flex-wrap: nowrap; gap: 0 10px; }
body[data-layout="compact"] #legend .strip.ground > strong.text { flex: 0 0 auto; }
body[data-layout="compact"] #legend .strip.ground > .close { order: 3; margin-left: auto; flex: 0 0 auto; }
body[data-layout="compact"] #legend .strip.ground .ground-keys { flex: 1 1 0; min-width: 0; padding-bottom: 0; }
body[data-layout="compact"] #legend .strip.ground .ground-keys .room { flex-basis: auto; }
body:is([data-layout="portrait"], [data-layout="compact"]):is([data-text-size="125"], [data-text-size="150"]) #legend .strip.ground .ground-keys .room { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; }
/* Large text on a phone: the Ground title and the hills key's rule are for screen readers only (the strip's aria-label still says Ground), so the legend fits the budget under a banner (tests/ui/mobileBudget.test.js). */
body:is([data-layout="portrait"], [data-layout="compact"]):is([data-text-size="125"], [data-text-size="150"]) #legend .strip.ground > strong.text, body:is([data-layout="portrait"], [data-layout="compact"]):is([data-text-size="125"], [data-text-size="150"]) #legend .strip.ground .hills-words { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; }
body[data-layout="wide"] #legend .strip.ground { display: inline-flex; flex-wrap: wrap; gap: 0 12px; }
body[data-layout="wide"] #legend .strip.ground > .close { order: 3; }
body[data-layout="wide"] #legend .strip.ground .ground-keys { padding-bottom: 0; }
body[data-layout="wide"] #legend .strip.ground .ground-keys .room { flex-basis: auto; }
/* Stage 4.7 (Task 4, ruling 465): the next-house spots' legend (ui/s47/spots.js), the Good and Fair keys each with the swatch the map draws, wrapping among
   themselves in the guidance row; and the Done button that ends the house tool (confirmBar.js), a word 44 px tall. */
#legend .strip.spots { flex-wrap: wrap; row-gap: 2px; column-gap: 14px; max-width: 100%; box-sizing: border-box; padding: 4px 12px; }
#legend .strip.spots .key { gap: 6px; }
#legend .strip.spots .spot-swatch { flex: 0 0 auto; display: block; }
.confirm .done { font-weight: 600; padding: 0 8px; font-size: calc(16px * min(var(--ui-scale), 1.15)); }   /* narrow as the x it replaces, so the bar keeps one height as plots come and go (tests/ui/placementFit.test.js) */
/* The Layers picker (placement review P2, ui/layersPicker.js): a popover of every overlay and Off. Three columns at 100% text,
   two from 125%, each button 44 px tall at least; the current layer is teal. On a phone it rides above the Layers corner
   button (its right edge on the button's; the left edge instead in left-handed mode); on a wide screen it opens under the
   Layers button below the minimap, over the tabs. It scrolls inside when a short screen cannot hold it. */
.layers-picker { position: absolute; z-index: 6; display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 6px; padding: 8px; box-sizing: border-box; background: var(--parchment); border: 1px solid var(--parchment-3); border-radius: var(--radius); box-shadow: var(--shadow); overflow-y: auto; }
.layers-picker .layer { position: relative; display: flex; align-items: center; min-height: var(--tap); padding: 2px 4px 2px 13px; overflow: hidden; font-size: calc(13px * var(--ui-scale)); line-height: 1.15; text-align: left; overflow-wrap: normal; hyphens: none; }
.layers-picker .layer .hue { position: absolute; left: 0; top: 0; bottom: 0; width: 6px; }   /* the overlay's own hue, as a stripe down the button's edge */
.layers-picker .layer[aria-pressed="true"] { background: var(--teal); color: var(--teal-ink); border-color: var(--teal); box-shadow: none; }
.layers-picker .layer.off { grid-column: 1 / -1; justify-content: center; }
body:is([data-text-size="125"], [data-text-size="150"]) .layers-picker { grid-template-columns: repeat(2, minmax(0, 1fr)); }
#corners .layers-picker { bottom: calc(100% + 8px); right: 8px; width: min(360px, calc(100% - 16px)); max-height: calc(100vh - 150px); max-height: calc(100dvh - 150px); }
body[data-hand="left"] #corners .layers-picker { right: auto; left: 8px; }
/* The popover stays on the screen: it hangs from the corner row itself (fixed, and as wide as the room the row has: in compact it stops left of the confirm bar
   or the card drawer while one shows), so a tool in compact never pushes it off the screen. On a phone at 125% text or more Off
   takes the last cell beside Ground (nine overlays and Off make five rows of two, not six); held sideways it is as tall as the
   top bar and the corner row leave (five tight rows fit even at 150% text), and the open popover scrolls the current choice into view (layersPicker.js). */
body:is([data-layout="portrait"], [data-layout="compact"]):is([data-text-size="125"], [data-text-size="150"]) .layers-picker .layer.off { grid-column: auto; }
body[data-layout="compact"] #corners .layers-picker { gap: 2px; padding: 4px; max-height: calc(100vh - var(--topbar-h) - var(--safe-top) - var(--safe-bottom) - 76px); max-height: calc(100dvh - var(--topbar-h) - var(--safe-top) - var(--safe-bottom) - 76px); overscroll-behavior: contain; }
/* Stage 4.5 (Health and Crime): twelve choices at 125% or 150% text would need six rows of two, taller than the short screen leaves; a wider popover (it grows leftwards, away from the confirm bar) holds them in four rows of three. */
body[data-layout="compact"]:is([data-text-size="125"], [data-text-size="150"]) #corners .layers-picker { grid-template-columns: repeat(3, minmax(0, 1fr)); width: min(540px, calc(100% - 16px)); }
#panel .layers-row { position: relative; flex: none; padding: 0 8px 8px; }
#panel .layers-button { width: 100%; font-size: calc(14px * var(--ui-scale)); }
#panel .layers-button[aria-pressed="true"] { border-color: var(--teal); box-shadow: inset 0 0 0 1px var(--teal); }
#panel .layers-picker { top: calc(100% - 4px); left: 8px; right: 8px; max-height: 60vh; grid-template-columns: repeat(2, minmax(0, 1fr)); }   /* the panel is 320 px: "Entertainment" does not fit a third of it */
#corners .overlay-button[aria-expanded="true"] { border-color: var(--teal); box-shadow: inset 0 0 0 1px var(--teal); }
/* A building that changes desirability has a fourth line in the confirm bar ("Makes 6 homes nicer"): in portrait the bar
   keeps room for it from the start (title 1.4em, three small lines of 1.05em; at 125% and 150% one of them may wrap). */
body[data-layout="portrait"] .confirm.desire .info { min-height: 4.55em; }
body[data-layout="portrait"]:is([data-text-size="125"], [data-text-size="150"]) .confirm.desire .info { min-height: 5.6em; }

/* Difficulty under New city (spec 4.18): three choices on one row where they fit, the chosen level's line beneath. The
   card scrolls rather than clip if a small phone at 150% text cannot hold it all. */
.title-card { max-height: 100%; overflow-y: auto; }
.title-difficulty { display: flex; flex-direction: column; align-items: center; gap: 4px; margin: 0 0 4px; }
.title-difficulty .choices { justify-content: center; }
.title-difficulty .difficulty-hint { margin: 0; white-space: pre-line; }   /* the hint is two lines: what buildings cost, then the road price */
#sheet .difficulty-note { margin: 4px 0 0; }
/* A phone held sideways has about 360 px of height (the title card shows before the layout is watched, so this is a
   height query): tighter spacing lets the card, difficulty row and all, fit at 100% text. */
@media (max-height: 480px) {
  .title-card { padding: 12px 24px; gap: 4px; }
  .title-card .tagline { margin: 0; }
  .title-buttons { gap: 4px; }
}

/* Papyrus (spec 6.9): the chrome's surfaces wear the fibre over their own colour; a box that sets its own background
   (the demolish bar's coral) keeps it plain. Nothing in the look moves, so it is the same under every Motion level. */
#topbar, #dock, #panel, #rail, #rail .drawer, #sheet, #sheet .head, #sheet .welcome-actions, .confirm, #dialog .box, .title-card { background-image: var(--look-fibre); }

/* (A stable separator between the Stage 3 sections: leave these lines as they are.) */

/* ── s3 contract (Track 0) ── Track 0's styles for the contract. Add new rules just above the end marker. */
/* Screenshot mode (spec 4.19, 6.4): nothing of the interface shows over the map; the screen-reader lines stay. */
body.screenshot #ui > :not(#live):not(#map-summary) { display: none !important; }
/* ── end s3 contract ── */

/* (A stable separator between the Stage 3 sections: leave these lines as they are.) */

/* ── s3 fire (Track A) ── Track A's styles for fire. Add new rules just above the end marker. */
/* ── end s3 fire ── */

/* (A stable separator between the Stage 3 sections: leave these lines as they are.) */

/* ── s3 raids (Track B) ── Track B's styles for raids and defence. Add new rules just above the end marker. */
/* ── end s3 raids ── */

/* (A stable separator between the Stage 3 sections: leave these lines as they are.) */

/* ── s3 art (Track C) ── Track C's styles for art and render. Add new rules just above the end marker. */
/* ── end s3 art ── */

/* (A stable separator between the Stage 3 sections: leave these lines as they are.) */

/* ── s3 ui (Track D) ── Track D's styles for the danger interface. Add new rules just above the end marker. */
/* D1: the one banner (spec 6.3), the wide screens' full banner: words first, then the buttons, which wrap under the words
   when they must and keep their 44 px. A raid warning's defence line sits on its own line; its countdown chip is the same
   row, one line. Phones have the one-line alert row below (.line). */
#banner .critical { flex-wrap: wrap; row-gap: 4px; }
#banner .critical .icon { flex: none; }
#banner .critical .text { flex: 1 1 6em; }
#banner .critical .defence { display: block; }
#banner .critical .buttons { flex: 0 1 auto; max-width: 100%; display: flex; flex-wrap: wrap; gap: 4px; margin-left: auto; }
#banner .critical .more { white-space: nowrap; }
#banner .critical.chip .expand { font-weight: 700; text-align: left; padding: 0 4px; }
/* The phone's alert row (ruling 167): one row, the icon, the words and View side by side. The words wrap to two lines at
   most and are cut with "…" past that; the full words are in the buttons' names, the alert list and the raid sheet. A
   raid warning's words are its countdown chip, a button that opens the raid sheet. */
#banner .critical.line { flex-wrap: nowrap; white-space: normal; }   /* normal: the goals strip's .chip rule would keep the words on one line */
#banner .critical.line .buttons { flex: none; flex-wrap: nowrap; }
#banner .critical.line strong, #banner .critical.line .warning { display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 2; line-clamp: 2; overflow: hidden; overflow-wrap: anywhere; }
#banner .critical.line .warning { font-weight: 700; text-align: left; padding: 0 4px; max-width: 100%; }
/* D2: an info caption (a blaze put out, a raid's report) is news, not a celebration: no star, a plain edge. */
#cheer .cheer.info { border-color: var(--parchment-3); }
#cheer .cheer.info .icon { display: none; }
#sheet .fire-note, #sheet .ambient-note { margin: 0 0 6px; }
/* D3: the danger sheets. The fire row's words wrap under its label; its Show me or View sits right under it. The
   festival and rubble groups keep their reason beside the button they explain; the tribute sheet's two outcome
   buttons share a row, neither styled as the default. */
#sheet .fire-row > :last-child { text-align: right; }
#sheet .fire-fix { display: flex; justify-content: flex-end; padding: 4px 0; }
#sheet .festival-group { display: flex; flex-direction: column; align-items: flex-start; gap: 4px; flex-basis: 100%; }
#sheet .festival-group p { margin: 0; }
#sheet .tribute-cost { font-weight: 700; margin: 0 0 6px; }
#sheet .cancels, #sheet .why-not { margin: 8px 0 4px; }
#sheet .tribute-actions button { flex: 1 1 10em; }
/* D4: the fire-risk legend: the four bands, then the dot and ring and what never burns on a row of their own. */
#legend .fire-keys { display: inline-flex; flex-wrap: wrap; align-items: center; gap: 2px 10px; order: 4; flex-basis: 100%; }
/* On a portrait phone the fire-risk legend has the guidance row to itself (ruling 164) and keeps it short: the name and the
   four bands share a row where they fit, the two firehouse keys sit beneath beside the x, and what never burns is kept
   for screen readers (the building sheet's fire row says it for each building). */
body[data-layout="portrait"] #legend .strip.graded:has(.fire-keys) { display: inline-grid; grid-template-columns: auto minmax(0, 1fr) auto; column-gap: 6px; row-gap: 0; padding-left: 10px; }
body[data-layout="portrait"] #legend .strip.graded:has(.fire-keys) > strong.text { grid-column: 1; grid-row: 1; }
body[data-layout="portrait"] #legend .strip.graded .scale:has(+ .fire-keys) { grid-column: 2 / 4; grid-row: 1; }
body[data-layout="portrait"] #legend .strip.graded .fire-keys { grid-column: 1 / 3; grid-row: 2; }
body[data-layout="portrait"] #legend .strip.graded:has(.fire-keys) .close { grid-column: 3; grid-row: 2; margin-left: 0; }
body[data-layout="portrait"] #legend .fire-keys .never-burn { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; }
/* ── end s3 ui ── */

/* (A stable separator between the Stage 3 sections: leave these lines as they are.) */

/* ── s3 score (Track E) ── Track E's styles for scoring and records. Add new rules just above the end marker. */
/* The score chip on the summary button (spec 4.19): out of the numbers' flow, at the button's bottom right, so the numbers
   keep their rows; the numbers sit 3 px higher to leave it room. */
#topbar .stats { position: relative; padding-bottom: 6px; }
#topbar .score-chip { position: absolute; right: 4px; bottom: 2px; line-height: 1; padding: 0 6px; border-radius: 8px; background: var(--teal-soft); color: var(--ink); font-weight: 600; white-space: nowrap; pointer-events: none; }
/* The summary's score section: the total and its medal, a bar per part with its number and reason, the splits. */
.score-section .score-total { margin: 4px 0 0; display: flex; align-items: baseline; flex-wrap: wrap; gap: 4px 8px; }
.score-section .score-number { font-size: calc(28px * var(--ui-scale)); line-height: 1.1; }
.score-section .medal { font-weight: 600; }
.score-section .medal::before { content: ""; display: inline-block; width: 0.8em; height: 0.8em; margin-right: 4px; border-radius: 50%; vertical-align: -0.08em; background: var(--parchment-3); }
.medal.bronze::before { background: #B0703A; } .medal.silver::before { background: #A9A9A9; } .medal.gold::before { background: var(--gold); }
.score-section p { margin: 2px 0; }
#sheet .score-parts { list-style: none; margin: 8px 0; padding: 0; display: grid; gap: 8px; }
.score-part .row { padding: 0; border: 0; }
.part-bar { display: block; height: 8px; border-radius: 4px; background: var(--parchment-2); box-shadow: inset 0 0 0 1px var(--parchment-3); overflow: hidden; }
.part-bar .fill { display: block; height: 100%; background: var(--teal); }
.score-section h4 { margin: 12px 0 4px; font-size: inherit; }
#sheet .splits { list-style: none; margin: 0; padding: 0; }
.splits li { padding: 2px 0; }
/* Seal this reign and Share score (spec 4.19). The emoji row keeps its lines; the link reads whole and can be selected. */
.seal-question { margin: 4px 0 8px; }
#sheet .emoji-row { margin: 8px 0; font: inherit; line-height: 1.5; white-space: pre; overflow-x: auto; }
#sheet .share-link-text { box-sizing: border-box; width: 100%; min-height: var(--tap); padding: 0 8px; font: inherit; color: var(--ink); background: var(--parchment-2); border: 1px solid var(--parchment-3); border-radius: var(--radius); }
#sheet .honour { margin: 4px 0; font-style: italic; }
/* The title card's records, This week's Nile and a friend's shared score (spec 4.1, 4.19). The card already scrolls inside
   the screen (the difficulty rules above) when they make it taller than a small phone. */
.title-records { display: flex; flex-direction: column; align-items: center; gap: 2px; margin: 2px 0; }
.title-records .best-line { margin: 0; }
.title-records .stamps { display: flex; flex-wrap: wrap; justify-content: center; align-items: center; gap: 6px; margin-top: 4px; }
.title-weekly { display: flex; flex-direction: column; gap: 2px; }
.title-weekly .weekly-hint { margin: 0; }
/* A phone held sideways (the height query the difficulty rules use): the week's seed line hides (the button still has it
   as its description, through aria-describedby) and This week's Nile shares a line with Play with sound on a wider card,
   so the card fits about 390 px of height at 100% text without scrolling. */
@media (max-height: 480px) {
  .title-card { width: min(440px, 100%); }
  .title-buttons { flex-direction: row; flex-wrap: wrap; }
  .title-buttons > * { flex: 1 1 100%; }
  .title-buttons > .title-weekly, .title-buttons > .sound { flex: 1 1 40%; }
  .title-weekly .weekly-hint { display: none; }
}
.shared-score { background: var(--parchment-2); border-radius: var(--radius); padding: 8px 12px; display: flex; flex-direction: column; gap: 2px; }
.shared-score h2 { margin: 0; font-size: calc(12px * var(--ui-scale)); color: var(--ink-soft); }
.shared-score p { margin: 0; }
.shared-score .beat { font-weight: 700; font-size: calc(20px * var(--ui-scale)); }
.shared-score .start-seed { margin-top: 4px; }
.title-card:has(> .guide) .shared-score { display: none; }   /* one panel at a time: the install guide or the new-city question */
.shared-bad { margin: 0; }
/* Feats (spec 4.19): their own group above the achievements, laid out as the achievements are. */
.feats-section { margin-top: 12px; }
#sheet .feats { list-style: none; margin: 4px 0; padding: 0; }
#sheet .feat { display: flex; align-items: center; justify-content: space-between; gap: 8px; padding: 6px 0; border-bottom: 1px solid var(--parchment-2); }
#sheet .feat .what { display: flex; flex-direction: column; min-width: 0; }
#sheet .feat .status { flex: none; white-space: nowrap; }
#sheet .feat.earned .status { color: var(--teal); font-weight: 600; }
#sheet .feat.lost .status { color: var(--ink-soft); }
/* The opening chain's Founded Thebes row, and the next-threshold bar (spec 4.1): a slim line under the goals strip. On a
   phone it shows only in calm portrait play (createUI's barAllowed: no banner, tool or guide; a tip, a caption or a sheet
   already takes the whole guidance row); a wide screen keeps it. */
#sheet .goals .founded { padding: 4px 0; }
#goals .threshold { display: inline-flex; align-items: center; gap: 8px; max-width: 100%; box-sizing: border-box; margin-top: 4px; background: var(--parchment); color: var(--ink); border-radius: var(--radius); padding: 0 12px; box-shadow: var(--shadow); line-height: 1.6; }
#goals .threshold-meter { flex: none; width: 48px; height: 6px; border-radius: 3px; background: var(--parchment-2); box-shadow: inset 0 0 0 1px var(--parchment-3); overflow: hidden; }
#goals .threshold-meter .fill { display: block; height: 100%; background: var(--teal); }
/* A citizen remark (spec 4.19): a quieter caption, with a speech mark for its icon and no gold. */
#cheer .cheer.remark { border-color: var(--parchment-3); }
#cheer .cheer.remark .text { font-weight: 400; font-style: italic; }
/* ── end s3 score ── */

/* (A stable separator between the Stage 3 sections: leave these lines as they are.) */

/* ── s3 balance (Track F) ── Track F's styles for balance and integration. Add new rules just above the end marker. */
/* ── end s3 balance ── */

/* (A stable separator between the Stage 3 and Stage 4 sections: leave these lines as they are.) */

/* ── s4 contract (Track 0) ── Stage 4's style rules, one section per track below. Ownership (the Stage 4 plan's "Shared
   files"): Track H owns the BASE of this file, everything above the first "s3" section marker (the :root tokens and the
   Stage 1-2 rules), and its own s4 look section; it restyles there and never edits a track's section. Every other track
   adds rules only inside its own s4 section, uses the tokens (var(--...)) rather than raw colours where a token exists,
   and names any custom property it adds with its prefix (--s4a- ... --s4f-). A track that needs a base rule changed asks
   the controller. Add new rules just above each section's end marker. */
/* ── end s4 contract ── */

/* (A stable separator between the Stage 4 sections: leave these lines as they are.) */

/* ── s4 sim (Track A) ── Track A's styles (none expected). */
/* ── end s4 sim ── */

/* (A stable separator between the Stage 4 sections: leave these lines as they are.) */

/* ── s4 art (Track B) ── Track B's styles (none expected: markers are drawn on the canvas). */
/* ── end s4 art ── */

/* (A stable separator between the Stage 4 sections: leave these lines as they are.) */

/* ── s4 ui (Track C) ── Track C's styles: the Monuments tray, the monument chips and confirm-bar lines, the site, guild,
   quarry, brickworks and palace sheets, the summary's Monuments section. */
/* A monument's line in the confirm bar (spec 6.5, ruling 251): two lines at most on a phone, the whole line in its name. */
.confirm .monument-line { color: var(--ink); }
body:is([data-layout="portrait"], [data-layout="compact"]) .confirm .monument-line { display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; }
/* The Ground legend's gold room swatch while a monument is placed (ruling 229). */
#legend .strip.ground .room .swatch.gold { display: inline-block; width: 14px; height: 10px; vertical-align: -1px; border: 1px solid var(--ink); border-radius: 2px; }
/* The site, finished monument and palace sheets' rows (spec 6.3): one line each, the progress bar full width under its words,
   a fix button (Show me, View) at the line's end. */
#sheet .s4-rows { list-style: none; margin: 0 0 8px; padding: 0; }
.s4-rows li { display: flex; flex-wrap: wrap; align-items: center; gap: 4px 8px; padding: 4px 0; border-bottom: 1px solid var(--parchment-3); }
.s4-rows li .text { flex: 1 1 12em; }
.s4-rows li .meter { flex: 1 0 100%; width: auto; height: 10px; }
.s4-rows li button { min-height: 44px; }
#sheet .actions .enlarge[disabled] { opacity: 1; color: var(--ink-soft); }
/* The tray's "more this way" hint (ruling 300, dock.js): a parchment fade over the tray's right end with a calm chevron, while
   the row scrolls further right. It is laid over the cards (top and height from the tray), so it adds no height; taps go
   through; it never moves (the opacity fade is instant under Gentle and Still, as all chrome). */
#dock .tray-more { position: absolute; right: 0; width: 44px; display: flex; align-items: center; justify-content: flex-end; padding-right: 6px; box-sizing: border-box; pointer-events: none; background: linear-gradient(to right, transparent, var(--parchment) 72%); transition: opacity 0.2s; }
#dock .tray-more[hidden] { display: none; }
#dock .tray-more .chev { display: flex; align-items: center; justify-content: center; width: 24px; height: 24px; border-radius: 50%; background: var(--parchment); box-shadow: 0 0 0 1px var(--parchment-3); color: var(--ink-soft); font-size: 20px; line-height: 1; font-weight: 600; }
/* ── end s4 ui ── */

/* (A stable separator between the Stage 4 sections: leave these lines as they are.) */

/* ── s4 score (Track D) ── Track D's styles: score v2's fifth bar, the splits, stamps' glyphs, captions. */
/* The opening goals' why (ruling 265): its own line under the goal's words, in the open goal and in the Goals sheet. */
.goal-strip .words .why:not([hidden]), #sheet .goals .goal .why { display: block; margin-top: 2px; }
/* ── end s4 score ── */

/* (A stable separator between the Stage 4 sections: leave these lines as they are.) */

/* ── s4 motion (Track E) ── Track E's styles: the Motion row, and the chrome's transitions per Motion level
   (body[data-motion]: instant under gentle and still, ruling 247). */
/* The interface's own transitions and animations (rows opening, the dock's heights, sheets, captions, the picture cards'
   slide) are instant under Gentle as under Still (spec 5.4): Gentle and Still differ only on the map. */
[data-motion="gentle"] *, [data-motion="gentle"] *::before, [data-motion="gentle"] *::after,
[data-motion="still"] *, [data-motion="still"] *::before, [data-motion="still"] *::after {
  transition-duration: 0s !important; transition-delay: 0s !important; animation: none !important;
}
/* The Motion row: the four choices, then what it is doing and what that level moves. */
#sheet .motion-row .motion-status { margin: 4px 0 0; font-weight: 600; }
#sheet .motion-row .motion-line { margin: 2px 0 8px; }
/* ── end s4 motion ── */

/* (A stable separator between the Stage 4 sections: leave these lines as they are.) */

/* ── s4 cards (Track G) ── Track G's styles: the picture cards' dialog, ring and arrow, dots, How to play, Music rows. */
/* The Music volume row: − value + with 44 px buttons (spec 5.5). */
.music-volume { align-items: center; }
.music-volume .stepper { display: flex; align-items: center; gap: 6px; }
.music-volume .stepper .value { min-width: 2.5em; text-align: center; font-variant-numeric: tabular-nums; }
.music-volume .stepper button { font-size: calc(20px * var(--ui-scale)); line-height: 1; padding: 0; }
/* How to play on the title card (spec 4.1) added a row to a card that on a phone held sideways (the height query the title
   rules above use) had no room: Play with sound and Install fell below the fold. There the five buttons sit in two rows of two
   (This week's Nile with How to play, Play with sound with Install), so the card is as tall as it was before How to play; and
   under 400 px (667x375) the tagline goes, so the last row ends inside the card too. */
@media (max-height: 480px) {
  .title-buttons > .how-to-play, .title-buttons > .install { flex: 1 1 40%; }
}
@media (max-height: 400px) {
  .title-card .tagline { display: none; }
}
/* The picture cards (spec 4.1): a full-screen sheet on a phone, the picture beside the words on a landscape phone and a
   centred card of at most 560 px on a wide screen. The dialog is its own size container (pc), so its parts are sized from
   its box, never the window's: a test's mounted shell lays it out as the phone it stands for. --s4g-pic is the picture's
   side: the picture keeps its shape and size, and only the words scroll when they do not fit (150% text). */
.picture-cards { position: fixed; inset: 0; z-index: 20; container: pc / size; display: flex; align-items: center; justify-content: center; background: rgba(43, 31, 14, 0.55); }
.picture-cards:focus { outline: none; }   /* tabindex -1: a click on the picture or the words puts focus on the dialog, so its keys keep working */
.picture-cards .pc-box {
  --s4g-pic: min(600px, 100cqw - 24px, 100cqh - 300px);
  box-sizing: border-box; width: 100%; height: 100%; overflow: hidden;
  display: grid; grid-template-columns: minmax(0, 1fr); grid-template-rows: auto auto minmax(0, 1fr) auto auto; gap: 8px;
  padding: max(8px, var(--safe-top)) max(12px, var(--safe-right)) max(10px, var(--safe-bottom)) max(12px, var(--safe-left));
  background: var(--parchment); color: var(--ink);
}
.picture-cards .pc-top { display: flex; align-items: center; gap: 4px; }
.picture-cards .pc-count { flex: 1; }
.picture-cards .pc-stage { touch-action: pan-y; }
.picture-cards .pc-figure { position: relative; margin: 0 auto; width: var(--s4g-pic); height: var(--s4g-pic); }
.picture-cards .pc-picture { display: block; width: 100%; height: 100%; border-radius: 8px; background: var(--parchment-2); object-fit: cover; }
.picture-cards .pc-mark { position: absolute; inset: 0; width: 100%; height: 100%; pointer-events: none; }
.picture-cards .pc-mark .halo { fill: none; stroke: #FFFFFF; stroke-width: 16; stroke-linecap: round; stroke-linejoin: round; }
.picture-cards .pc-mark .line { fill: none; stroke: var(--teal); stroke-width: 8; stroke-linecap: round; stroke-linejoin: round; }
.picture-cards .pc-text { min-height: 0; overflow-y: auto; }
.picture-cards .pc-text h2 { font-size: calc(20px * var(--ui-scale)); line-height: 1.25; margin: 0 0 4px; }
.picture-cards .pc-text p { margin: 0; }
.picture-cards .pc-dots { display: flex; justify-content: center; }
.picture-cards .pc-dot { position: relative; min-width: 44px; min-height: 44px; padding: 0; border: 0; background: transparent; }
.picture-cards .pc-dot::after { content: ''; position: absolute; left: 50%; top: 50%; width: 12px; height: 12px; margin: -6px 0 0 -6px; box-sizing: border-box; border-radius: 50%; border: 2px solid var(--ink-soft); }
.picture-cards .pc-dot[aria-current="step"]::after { width: 16px; height: 16px; margin: -8px 0 0 -8px; background: var(--teal); border-color: var(--teal); }
.picture-cards .pc-nav { display: flex; justify-content: space-between; gap: 8px; }
.picture-cards .pc-next { flex: 0 1 auto; min-width: 40%; }
/* A landscape phone: the picture on the left, the words, the dots and the buttons beside it. */
@container pc (min-aspect-ratio: 1 / 1) and (max-height: 599px) {
  .picture-cards .pc-box { --s4g-pic: min(600px, 100cqh - 20px, 50cqw); grid-template-columns: var(--s4g-pic) minmax(0, 1fr); grid-template-rows: auto minmax(0, 1fr) auto auto; column-gap: 16px; }
  .picture-cards .pc-stage { grid-column: 1; grid-row: 1 / 5; align-self: center; }
  .picture-cards .pc-top, .picture-cards .pc-text, .picture-cards .pc-dots, .picture-cards .pc-nav { grid-column: 2; }
}
/* A wide screen: a centred card. */
@container pc (min-width: 900px) and (min-height: 600px) {
  .picture-cards .pc-box { --s4g-pic: min(536px, 100cqh - 330px); width: min(560px, 100cqw - 32px); height: auto; max-height: calc(100cqh - 32px); border-radius: var(--radius); box-shadow: var(--shadow); }
}
/* A phone held upright (art-direction review 9): the square picture is as wide as the phone allows, so the height it
   leaves goes evenly above the picture and below its words (two spacer rows), the picture and the words staying together
   in the middle of the sheet instead of a gap under the words; the dots and the buttons keep the foot. A landscape phone
   puts the picture beside the words (below), so there the items go back to its own rows. */
.picture-cards .pc-box { grid-template-rows: auto minmax(0, 1fr) auto minmax(0, auto) minmax(0, 1fr) auto auto; }
.picture-cards .pc-top { grid-row: 1; }
.picture-cards .pc-stage { grid-row: 3; }
.picture-cards .pc-text { grid-row: 4; }
.picture-cards .pc-dots { grid-row: 6; }
.picture-cards .pc-nav { grid-row: 7; }
@container pc (min-aspect-ratio: 1 / 1) and (max-height: 599px) {
  .picture-cards .pc-box { grid-template-rows: auto minmax(0, 1fr) auto auto; }
  .picture-cards .pc-top { grid-row: 1; }
  .picture-cards .pc-text { grid-row: 2; }
  .picture-cards .pc-dots { grid-row: 3; }
  .picture-cards .pc-nav { grid-row: 4; }
  .picture-cards .pc-stage { grid-row: 1 / 5; }
}
@container pc (min-width: 900px) and (min-height: 600px) {   /* the centred card is as tall as what it holds */
  .picture-cards .pc-box { grid-template-rows: auto auto minmax(0, 1fr) auto auto; }
  .picture-cards .pc-stage { grid-row: 2; }
  .picture-cards .pc-text { grid-row: 3; }
  .picture-cards .pc-dots { grid-row: 4; }
  .picture-cards .pc-nav { grid-row: 5; }
}
/* Under Full motion only (src/ui/tutorial.js adds the classes): the new card slides in from the side it came from. */
@keyframes s4g-in-next { from { transform: translateX(32px); opacity: 0.2; } to { transform: none; opacity: 1; } }
@keyframes s4g-in-back { from { transform: translateX(-32px); opacity: 0.2; } to { transform: none; opacity: 1; } }
.picture-cards .pc-in-next { animation: s4g-in-next 200ms ease-out; }
.picture-cards .pc-in-back { animation: s4g-in-back 200ms ease-out; }
/* ── end s4 cards ── */

/* (A stable separator between the Stage 4 sections: leave these lines as they are.) */

/* ── s4 look (Track H) ── Track H's own new rules (the frieze, cartouches, gold-on-lapis primary buttons, fibre,
   ornaments) beside its edits to the base above the s3 sections. */
/* The frieze (spec 6.9): blocks of lapis, gold, terracotta and turquoise parted by ink, on an ink line; 4 px in all. The
   title card's one glyph: an ankh in lapis. */
:root {
  --look-frieze: linear-gradient(var(--ink), var(--ink)) 0 100% / 100% 1px no-repeat,
    repeating-linear-gradient(90deg, var(--lapis) 0 11px, var(--ink) 11px 12px, var(--gold) 12px 23px, var(--ink) 23px 24px, var(--terracotta) 24px 35px, var(--ink) 35px 36px, var(--turquoise) 36px 47px, var(--ink) 47px 48px);
  --look-ankh: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 24'%3E%3Cg fill='none' stroke='%231C3768' stroke-width='2.4' stroke-linecap='round'%3E%3Cellipse cx='8' cy='6' rx='3.6' ry='4.6'/%3E%3Cpath d='M1.5 12.5h13M8 11v11.5'/%3E%3C/g%3E%3C/svg%3E");
}
/* Along the top bar's lower edge and each sheet's head: over the 1 px border and 3 px inside it, so neither grows. Both
   boxes make their own stacking context (fixed; sticky with a z-index), so z-index -1 lays the frieze over their papyrus
   and under their buttons: a 44 px button that reaches the edge keeps its own outline. */
#topbar::after, #sheet .head::after { content: ''; position: absolute; left: 0; right: 0; bottom: -1px; height: 4px; z-index: -1; background: var(--look-frieze); pointer-events: none; }
.title-card { position: relative; }
.title-card::before { content: ''; position: absolute; left: 0; right: 0; top: 0; height: 4px; background: var(--look-frieze); pointer-events: none; }   /* in the card's top padding */
/* Cartouches (spec 6.9; the controller's ruling 18): a sheet's title and the city's name, and only those, sit in a
   cartouche in the serif display face: an outline of soft ink, round at the start and drawn in at the tie's end, where
   the tie (a bar across the end, flush inside it, no taller) closes it, so the two read as one shape. Body text
   stays the system sans. The outline is an inset shadow and the tie is absolute, so neither takes room: a title that
   wraps grows the head no more than it did. The sheet's title hugs its words (the ✕ keeps the far end); the name fits
   a 375 px phone at 150% text. */
#sheet .head h2 { flex: 0 1 auto; min-width: 0; margin-right: auto; position: relative; padding: 0 14px; border-radius: 0.75em 0.3em 0.3em 0.75em; box-shadow: inset 0 0 0 1.5px var(--ink-soft); font-family: var(--serif); letter-spacing: 0.02em; }
.title-card h1 { align-self: center; position: relative; display: inline-flex; align-items: center; gap: 0.25em; padding: 0 0.6em 0 0.5em; border-radius: 0.75em 0.3em 0.3em 0.75em; box-shadow: inset 0 0 0 2px var(--ink); font-family: var(--serif); letter-spacing: 0.06em; }
#sheet .head h2::after, .title-card h1::after { content: ''; position: absolute; border-radius: 1px; pointer-events: none; }
/* The tie (art-direction review 5, ruling 18: it joins the oval or goes): a bar inside the outline's square end, flush
   with it top, bottom and right and no taller, so the end reads as the knotted end of one loop, not a cursor beside it. */
#sheet .head h2, .title-card h1 { border-top-right-radius: 2px; border-bottom-right-radius: 2px; }
#sheet .head h2::after { right: 0; top: 0; bottom: 0; width: 4px; border-radius: 0 2px 2px 0; background: var(--ink-soft); }
.title-card h1::after { right: 0; top: 0; bottom: 0; width: 5px; border-radius: 0 2px 2px 0; background: var(--ink); }
.title-card h1 { letter-spacing: 0.04em; }
/* A section's title ("Stock", "Employment") is quieter: the serif face at a normal weight with a thin ink rule running
   from its words to the end of the row, no cartouche. It gains 14 px of margin (10 above, 4 below). */
#sheet h3 { display: flex; align-items: center; gap: 10px; margin: 10px 0 4px; font-family: var(--serif); font-weight: 400; font-size: calc(17px * var(--ui-scale)); letter-spacing: 0.02em; }
#sheet h3::after { content: ''; flex: 1 1 0; min-width: 24px; height: 1px; background: var(--ink-line); pointer-events: none; }
/* The title card's one glyph, inside the cartouche before the name: empty generated content, so a screen reader meets only
   "Thebes"; a sheet's head has none (at most one, spec 6.9). */
.title-card h1::before { content: ''; flex: none; width: 0.5em; height: 0.75em; background: var(--look-ankh) center / contain no-repeat; }
/* A toast (art-direction review 10): lapis with the gold ink of the main buttons (8.4:1) and a thin gold edge, instead of
   the near-black pill that was the one piece of chrome left out of the look; its action in parchment. */
.toast { background: var(--lapis); color: var(--lapis-ink); box-shadow: var(--shadow), inset 0 0 0 1px rgba(245, 217, 138, 0.45); }
.toast button { color: var(--parchment); text-decoration: underline; text-underline-offset: 3px; }
.toast button:focus-visible { outline-color: var(--lapis-ink); }
/* ── end s4 look ── */

/* (A stable separator between the Stage 4 sections: leave these lines as they are.) */

/* ── s4 balance (Track F) ── Track F's styles for integration fixes. */
/* The title backdrop's showcase (art-direction review 4): the backdrop frames the sample city's monuments in the largest
   band the card leaves free (src/ui/titleCard.js showcaseFrame). A phone held upright puts the card at the top, so the
   band under it is as tall as it can be; a phone held sideways puts it at the right, so the band beside it is. */
@media (orientation: portrait) and (max-height: 900px) {
  #title { align-items: flex-start; padding-top: max(12px, env(safe-area-inset-top)); }
}
@media (orientation: landscape) and (max-height: 480px) {
  #title { justify-content: flex-end; padding-right: max(12px, env(safe-area-inset-right)); }
}
/* ── end s4 balance ── */

/* ── s45 (Stage 4.5, Task 4b) ── the forecast in the gold chip, and the cost lines on cards and the confirm bar. */
/* It sits under the gold number, not beside it, and adds no width of its own (a grid whose chip is as wide as the gold line; the
   forecast's words run on beneath the neighbouring numbers, which sit at the top of the row), so a phone's four numbers keep their
   one row at every text size and the forecast adds a second line inside the bar's own height. Where the numbers have already wrapped
   to two rows the bar has no room for a third line, and hud.js hides it (class tight). */
#topbar .stats { align-items: flex-start; }
#topbar .stat.gold { display: grid; grid-auto-flow: column; justify-content: start; align-items: center; column-gap: 3px; }
#topbar .stat.gold .forecast { grid-column: 1 / span 3; grid-row: 2; width: 0; overflow: visible; justify-self: start; }
#topbar .stats.tight .forecast { display: none; }
#topbar .forecast { font-size: calc(12px * var(--ui-scale)); line-height: 1.2; color: var(--ink-soft); white-space: nowrap; cursor: pointer; }
#topbar .forecast .short { display: none; }
body:is([data-layout="portrait"], [data-layout="compact"]) #topbar .forecast .long { display: none; }
body:is([data-layout="portrait"], [data-layout="compact"]) #topbar .forecast .short { display: inline; }
/* Amber only when the sim says the treasury runs out within its window; the warning mark in the words says so as well. */
#topbar .forecast.warn { color: var(--amber-text); font-weight: 600; }
.card .upkeep { margin-top: -3px; font-size: calc(12px * var(--ui-scale)); line-height: 1.15; color: var(--ink-soft); }
/* The upkeep line is up to two lines on a 124 px card (a farm's has the ground line besides), so a card gives back a few px to keep every tray inside --dock-tray. */
.card { gap: 2px; padding: 5px 8px; }
/* At 125% text and more the card keeps "Costs 46 a month" and leaves "· 10 workers" to screen readers and the tip (a wrapped third line would push the tray past its height). */
body:is([data-text-size="125"], [data-text-size="150"]) .card .upkeep .crew { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; }
.card .thumb { height: 47px; }
#confirm .balance { white-space: normal; }
/* A building with a monthly bill has one more line (its balance): the portrait bar's info keeps its room for it. */
body[data-layout="portrait"] .confirm.bal .info { min-height: 4.65em; }
body[data-layout="portrait"]:is([data-text-size="125"], [data-text-size="150"]) .confirm.bal .info { min-height: 5.7em; }
body[data-layout="portrait"] .confirm.desire.bal .info { min-height: 5.7em; }
body[data-layout="portrait"]:is([data-text-size="125"], [data-text-size="150"]) .confirm.desire.bal .info { min-height: 6.75em; }
#tip .tip-row .tip-show-me { flex: none; white-space: nowrap; }
body:is([data-layout="portrait"], [data-layout="compact"]):is([data-text-size="125"], [data-text-size="150"]) #tip .tip-row .tip-show-me { padding: 0 6px; font-size: calc(14px * var(--ui-scale)); }
/* ── end s45 ── */

/* ── s46 map (Map track) ── the Map track's styles. Add new rules just above the end marker. */
/* The map size picker under the difficulty on the title card (ruling 333), laid out as the difficulty row is. */
.title-size { display: flex; flex-direction: column; align-items: center; gap: 4px; margin: 4px 0 0; }
.title-size .choices { justify-content: center; }
.title-size .size-hint { margin: 0; }
/* A phone held sideways (the height query the title rules above use): the size picker is one row, its label beside the three buttons, and its
   line hides (each button still has its size in its name, and the line is its description), so the card still fits without scrolling. */
@media (max-height: 480px) {
  .title-size { flex-direction: row; justify-content: center; gap: 8px; margin: 0; }
  .title-size .size-hint { display: none; }
  /* The card is wider there, so the four buttons under it (This week's Nile, How to play, Play with sound, Install) share one row, which pays for the picker's row. */
  .title-card { width: min(680px, 100%); }
  .title-buttons > .title-weekly, .title-buttons > .how-to-play, .title-buttons > .sound, .title-buttons > .install { flex: 1 1 22%; }
}
/* Show me in the confirm bar (rulings 331, 350): a button 44px high across a row of its own, under the words, buttons and chips. */
.confirm.has-fit { flex-wrap: wrap; row-gap: 6px; }
.confirm .show-fit, .confirm .show-fit-room { flex: 1 0 100%; order: 10; min-height: var(--tap); }   /* the room is the empty row a terrain building keeps for Show me, so the bar holds one height */
/* A terrain building's bar on a portrait phone stays under half of a small screen's height at the largest text (UX rule 2): the words column
   beside the three buttons is only ~150px wide, so its lines wrapped to two or three each. The words flow instead as the bar's own rows (the
   title beside the buttons, then each line across the whole width), so a line the width holds takes one row, not two. */
body[data-layout="portrait"] .confirm.has-fit { row-gap: 0; }
body[data-layout="portrait"] .confirm.has-fit .info { display: contents; }
body[data-layout="portrait"] .confirm.has-fit .info > .lines { order: 3; flex: 1 0 100%; min-height: calc(3 * 1.45 * 12px * var(--ui-scale)); }   /* room for the three lines a warning or an effect takes, so the bar does not bob */
body[data-layout="portrait"] .confirm.has-fit .info > .title { order: 0; flex: 1 1 0; min-width: 0; }
body[data-layout="portrait"] .confirm.has-fit .conds, body[data-layout="portrait"] .confirm.has-fit .show-fit, body[data-layout="portrait"] .confirm.has-fit .show-fit-room { margin-top: 6px; }
/* ── end s46 map ── */

/* ── s46 river (River track) ── the River track's styles. Add new rules just above the end marker. */
/* ── end s46 river ── */

/* ── s46 court (Court track) ── the Court track's styles. Add new rules just above the end marker. */
/* The letter sheet: the ask, its rows, then each choice as a button with what it does under it (every choice states its exact effect). */
#sheet .letter h3.letter-ask { margin: 8px 0 4px; font-family: var(--serif); font-size: calc(18px * var(--ui-scale)); }
#sheet .letter .letter-body { margin: 4px 0 8px; }
#sheet .letter .small { margin: 4px 0; }
#sheet .court-choices { display: grid; gap: 14px; margin: 14px 0 6px; }
#sheet .court-choice { display: flex; flex-direction: column; gap: 4px; }
#sheet .court-choice > button { width: 100%; padding: 8px 12px; white-space: normal; }
#sheet .court-choice > button:disabled { opacity: 0.55; }
#sheet .court-choice p { margin: 0; }
#sheet .letter-foot { display: flex; flex-wrap: wrap; gap: 8px; margin: 12px 0 0; }
/* The court sheet and the summary's row: favour is an icon, a number and words, with a bar that repeats it and marks the two thresholds. */
#sheet .favour-head { display: flex; flex-wrap: wrap; align-items: baseline; gap: 4px 8px; margin: 8px 0 6px; }
#sheet .favour-mark { font-size: calc(18px * var(--ui-scale)); line-height: 1; }
#sheet .favour-number { font-family: var(--serif); font-size: calc(28px * var(--ui-scale)); line-height: 1; }
#sheet .favour-band { font-weight: 600; margin-left: auto; }
#sheet .favour-head.high .favour-mark, #sheet .favour-head.high .favour-band { color: var(--teal); }
#sheet .favour-head.low .favour-mark, #sheet .favour-head.low .favour-band { color: var(--coral-text); }
#sheet .favour-meter { position: relative; height: 12px; margin: 4px 0 8px; border: 1px solid var(--ink-line); border-radius: 6px; background: var(--parchment-2); overflow: hidden; }
#sheet .favour-meter .fill { position: absolute; top: 0; bottom: 0; left: 0; background: var(--ink-soft); }
#sheet .favour-meter.high .fill { background: var(--teal); }
#sheet .favour-meter.low .fill { background: var(--coral-text); }
#sheet .favour-meter .tick { position: absolute; top: 0; bottom: 0; width: 2px; margin-left: -1px; background: var(--ink); opacity: 0.6; }
#sheet .favour-now { margin: 4px 0; font-weight: 600; }
#sheet .letters-waiting, #sheet .letters-past, #sheet .units-away { list-style: none; padding-left: 0; margin: 4px 0; }
#sheet .letters-waiting li, #sheet .letters-past li, #sheet .units-away li { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: 4px 8px; padding: 4px 0; border-bottom: 1px solid var(--parchment-2); }
#sheet .letters-past li { display: block; }
#sheet .court-actions { display: flex; flex-wrap: wrap; gap: 8px; margin: 8px 0; }
/* A letter in the alert row: a letter is news, not harm, so it is teal with an envelope (the words say it too), not the coral warning. */
#banner .critical[data-code="court_letter"] { background: var(--teal-soft); border-color: var(--teal); }
#banner .critical[data-code="court_letter"] .icon { display: none; }
#banner .critical[data-code="court_letter"]::before { content: '\2709'; flex: none; color: var(--teal); font-size: calc(18px * var(--ui-scale)); line-height: 1; }
/* ── end s46 court ── */

/* ── s46 armies (Armies track) ── the Armies track's styles. Add new rules just above the end marker. */
/* ── end s46 armies ── */

/* ── s46 wonders (Wonders track) ── the Wonders track's styles. Add new rules just above the end marker. */
/* The style picker in a monument's confirm bar (src/ui/confirmBar.js, ruling 343): a row of its own at the foot of the bar, three buttons that each
   share the width, the one in force teal with a tick (and aria-checked: never colour alone), and one line under them for what it costs and does. */
.confirm.has-style { flex-wrap: wrap; row-gap: 6px; }
.confirm .style-row { flex: 1 0 100%; order: 11; display: flex; flex-direction: column; gap: 4px; }
.confirm .style-choices { display: flex; flex-wrap: wrap; gap: 6px; }
/* Ruling 420: each button starts at its own words' width and the row shares out what is left, so "✓ Whitewashed" (the widest, bold when in force)
   is never cut to "Whitewashe" in the wide layout's 320 px side panel; where the three cannot fit one row they wrap to a second, never clip. */
.confirm .style-pick { flex: 1 1 auto; min-width: var(--tap); min-height: var(--tap); padding: 0 6px; font-size: calc(13px * min(var(--ui-scale), 1.15)); line-height: 1.2; white-space: nowrap; }   /* the text size is capped here, so the three keep one row on a 375 px phone at 150% */
.confirm .style-pick[aria-pressed="true"] { background: var(--teal); color: var(--teal-ink); border-color: var(--teal); font-weight: 600; }
.confirm .style-line { color: var(--ink); font-size: calc(12px * var(--ui-scale)); }
.style-row-text { margin: 4px 0; }
/* ── end s46 wonders ── */

/* ── s46 ux (UX track) ── the UX track's styles: the top bar and speed control, and the shared legibility tokens (it may also edit
   the :root tokens at the top of this file, in their own lines). Add new rules just above the end marker. */
/* Ruling 400: the Enter hint on the confirm bar's tick. Only where there is a keyboard: body[data-keyboard] is set by keyboard.js, 'fine' when
   a mouse or trackpad is attached (the wide layout shows it then) and 'used' once a real key has been pressed (any layout). The hint stacks
   under the tick so the button stays 44 px tall and grows by a word's width, not a second button's. */
.confirm .ok .keyhint { display: none; }
body[data-keyboard="used"] .confirm .ok, body[data-keyboard="fine"][data-layout="wide"] .confirm .ok { display: inline-flex; flex-direction: column; align-items: center; justify-content: center; gap: 0; padding: 0 8px; line-height: 1.1; }
body[data-keyboard="used"] .confirm .ok .keyhint, body[data-keyboard="fine"][data-layout="wide"] .confirm .ok .keyhint { display: block; font-size: calc(13px * var(--ui-scale)); letter-spacing: 0.02em; opacity: 0.95; }
body[data-keyboard="used"] .confirm .ok .icon, body[data-keyboard="fine"][data-layout="wide"] .confirm .ok .icon { width: auto; }

/* Fix round 1, ruling 409 (phone bar): on an upright phone the words sit beside the buttons by design (the bar keeps one height while the ghost
   moves, and the corner buttons sit on it; stacking would cover a further row of map). A keyboard on a phone is rare, and the hint widened the tick
   and squeezed the words further, so the portrait bar shows the tick alone (its title and aria-keyshortcuts still say Enter). The landscape-phone
   (compact) bar already stacks its words over the buttons, so it keeps the hint. */
body[data-layout="portrait"] .confirm .ok .keyhint { display: none !important; }
body[data-layout="portrait"][data-keyboard="used"] .confirm .ok { display: inline-flex; flex-direction: row; padding: 0; }
/* Ruling 404: the wide top bar's steps. Pause and the three speeds are one row of 44 px steps, each with its picture (pause bars, then one, two
   and three arrows); the pressed one is the state and alone carries its word ("Paused", "1×", "2×", "3×"). It is filled: lapis with gold ink
   while playing, amber with a heavy edge while paused, so the two states differ in the picture, the word and the shape of the fill and not
   only a colour. Only the pressed step has a word, so the row stays about as wide as the old four words were (the numbers beside it keep one row). */
#topbar .speed-bar { display: flex; gap: 4px; align-items: stretch; }
.speed-bar .speed-step { display: inline-flex; align-items: center; justify-content: center; gap: 0.4em; padding: 0 8px; font-weight: 600; white-space: nowrap; }
.speed-bar .speed-step .icon { width: auto; }
.speed-bar .speed-step:not([aria-pressed="true"]) .word { display: none; }
.speed-bar .speed-step[aria-pressed="true"] { background: var(--lapis); color: var(--lapis-ink); border-color: var(--lapis); box-shadow: inset 0 0 0 2px var(--lapis), inset 0 0 0 3px rgba(245, 217, 138, 0.5); }
.speed-bar .step-0[aria-pressed="true"] { background: var(--amber-soft); color: var(--ink); border-color: var(--amber-text); box-shadow: inset 0 0 0 2px var(--amber-text); }

/* Ruling 419 (Josh: playing or paused should be unmistakable): the speed pictures are drawn (src/ui/speed.js speedPicture), all one height that
   grows with the text size: pause is two solid bars with a clear gap, play a triangle of the same height and weight. On a phone the corner button
   says the state as well as the action: paused it turns amber with a heavy edge (the wide bar's Paused step) and a "Paused" tag sits on its top
   edge, outside its height, so the corner row never grows; running it stays lapis with the pause bars and the speed. */
.speed-picture { display: inline-flex; align-items: center; justify-content: center; line-height: 0; }
.speed-picture svg { display: block; height: calc(14px * var(--ui-scale)); width: auto; }
#corners .speed-main { position: relative; }
#corners .speed-main[data-state="paused"] { background: var(--amber-soft); color: var(--ink); border-color: var(--amber-text); box-shadow: inset 0 0 0 2px var(--amber-text); }
#corners .speed-main .state-tag { display: none; }
body.nudge-play #corners .speed-main[data-state="paused"] { box-shadow: 0 0 0 3px var(--gold), inset 0 0 0 2px var(--amber-text); }   /* the Play tip's gold ring still shows on the paused button */
#corners .speed-main[data-state="paused"] .state-tag { display: block; position: absolute; left: 50%; top: 0; transform: translate(-50%, -55%); padding: 0 6px; border-radius: 6px; background: var(--amber-text); color: #FFFFFF; font-size: calc(12px * var(--ui-scale)); font-weight: 600; line-height: 1.3; white-space: nowrap; pointer-events: none; }

/* Ruling 407, finding F1: on a wide screen under an alert the overlay legend kept only its dot and ring ("Has water", "No water" read by screen readers
   only), so a sighted player met two unexplained marks. The words come back: the strip grows sideways, not taller. */
body.has-banner[data-layout="wide"] #legend .strip:not(.graded) .text { position: static; width: auto; height: auto; overflow: visible; clip: auto; white-space: normal; }

/* Ruling 407, finding F3: on a wide screen the confirm bar's buttons sat beside its words and left them about 60 px (a road's cost read one word a
   line). The words take the full row and the buttons go under them, at the right. */
body[data-layout="wide"] .confirm { flex-wrap: wrap; justify-content: flex-end; row-gap: 6px; }
body[data-layout="wide"] .confirm .info { flex: 1 1 100%; }

/* Ruling 407, finding F2: at 125% and 150% text seven category labels do not fit a phone's width ("Monuments" ran off the edge and the words touched).
   The row scrolls sideways instead, from the left, and a fade at the right edge shows there is more; the icons keep their size. */
body[data-text-size="125"] #dock .cats, body[data-text-size="150"] #dock .cats { justify-content: flex-start; gap: 2px; overflow-x: auto; scrollbar-width: none; scroll-snap-type: x proximity; }
body[data-text-size="125"] #dock .cats::-webkit-scrollbar, body[data-text-size="150"] #dock .cats::-webkit-scrollbar { display: none; }
body[data-text-size="125"] #dock .cat, body[data-text-size="150"] #dock .cat { flex: 0 0 auto; scroll-snap-align: start; padding: 2px 6px; }
body[data-text-size="150"][data-layout="portrait"] #dock .cats { -webkit-mask-image: linear-gradient(90deg, #000 calc(100% - 24px), transparent); mask-image: linear-gradient(90deg, #000 calc(100% - 24px), transparent); }

/* Ruling 407, finding F5: the score chip sat on the frieze under the bar; lifted clear of it. */
#topbar .score-chip { bottom: 6px; }
/* Ruling 405, finding F4: the cartouche's tie was a 4 px bar flush against a square end, with a round end opposite: heavy on one side, it read
   as a glitch in the sheet's heading. Both ends are round now and the tie is a thin bar set in from the right end, inside the outline, so the
   heading still reads as a cartouche (a loop with a tie) and nothing sits heavier than the line itself. */
#sheet .head h2, .title-card h1 { border-radius: 0.75em; }
/* Final review (ruling 450): the tie still read as a text cursor after "Thebes" and every sheet heading, so it is gone: the round loop alone is the
   cartouche, and the heading keeps its even padding. (Fix round 1, ruling 409, had moved the tie onto the loop's end; it still read as a caret.) */
#sheet .head h2::after, .title-card h1::after { content: none; }
/* Rulings 401-403: the version line under the tagline, the What's new button, and its panel (the title card's one panel at a time, with Back).
   The panel scrolls inside the card; its Back sits at the top and the foot. Lists read as plain bullets, New and Fixed in small headings. */
.title-card .version { margin: -4px 0 4px; letter-spacing: 0.02em; }
.title-card .whats-new { text-align: left; max-height: max(260px, calc(100dvh - 190px)); overflow-y: auto; padding-right: 4px; }
.title-card .whats-new-head { display: flex; align-items: center; gap: 10px; position: sticky; top: 0; z-index: 1; background: var(--parchment); padding-bottom: 6px; }
.title-card .whats-new-head h2 { font-family: var(--serif); font-size: calc(20px * var(--ui-scale)); margin: 0; }
.title-card .release { padding: 8px 0 4px; border-top: 1px solid var(--parchment-3); }
.title-card .release h3 { font-size: calc(16px * var(--ui-scale)); margin: 0; }
.title-card .release .newest { color: var(--teal); font-weight: 600; }
.title-card .release .release-title { margin: 0 0 2px; color: var(--ink-soft); }
.title-card .release h4 { margin: 6px 0 2px; font-size: calc(12px * var(--ui-scale)); text-transform: uppercase; letter-spacing: 0.06em; color: var(--ink-soft); }
.title-card .release ul.notes { margin: 0; padding-left: 20px; }
.title-card .release ul.notes li { margin: 2px 0; }
/* On a phone held upright How to play and What's new share one row (the DOM keeps How to play beside Play with sound, which tests pin; order lays
   the pair above Play with sound and Install), so the new button costs the card no row and the sample city's showcase keeps its room. */
.title-buttons > .how-to-play, .title-buttons > .whats-new-button { order: 5; }
.title-buttons > .sound, .title-buttons > .install { order: 6; }
@media (min-height: 481px) {
  .title-buttons { flex-flow: row wrap; }
  .title-buttons > * { flex: 1 1 100%; }
  .title-buttons > .how-to-play, .title-buttons > .whats-new-button { flex: 1 1 calc(50% - 4px); }
}
/* A short phone held upright (360x640): the version line costs the card a line, which the sample city's showcase below it needed (the
   title scene's balance test); the card's own gaps give it back. */
@media (min-height: 481px) and (max-height: 700px) {
  .title-card { padding-top: 18px; padding-bottom: 18px; gap: 6px; }
  .title-card .tagline { margin: 0; }
  .title-card .version { margin: -4px 0 0; }
}
.title-card:has(> .guide .whats-new) > .tagline, .title-card:has(> .guide .whats-new) > .version { display: none; }   /* the list takes the card's room */
/* A phone held sideways: with What's new the card has five small buttons (This week's Nile, How to play, What's new, Play with sound, Install);
   they size by their words and share one row where they fit (they did in two rows of two before), so the card still ends inside 390 px. */
@media (max-height: 480px) {
  .title-card .version { margin: -2px 0 -3px; line-height: 1.2; }
  .title-buttons > .title-weekly, .title-buttons > .how-to-play, .title-buttons > .whats-new-button, .title-buttons > .sound, .title-buttons > .install { flex: 1 1 auto; }
  .title-card .whats-new { max-height: 60vh; }
}
/* At 667x375 the card has no spare line (the tagline is already gone): the version sits in the card's bottom padding instead of a row of its own. */
@media (max-height: 400px) {
  .title-card { padding: 8px 24px 18px; }
  .title-card .version { position: absolute; left: 0; right: 0; bottom: 2px; margin: 0; line-height: 1.2; }
}
/* ── end s46 ux ── */

/* Stage 4.7 (Task 6, ruling 467): the Fetch control on a store's sheet (src/ui/s47/carts.js). One panel: a "Fetch goods" row that opens the list of goods the
   store may hold, each a switch and a stepper (44 px targets, wrapping under the switch on a narrow phone), with the sim's words under it. Goods on the road are
   said under the stock rows. */
#sheet .fetch-panel { margin: 8px 0; scroll-margin-top: 60px; }
#sheet .fetch-head { width: 100%; display: flex; align-items: center; gap: 8px; text-align: left; }
#sheet .fetch-head .label { font-weight: 600; flex: 1 1 auto; }
#sheet .fetch-head .summary { flex: none; }
#sheet .fetch-head .chev { flex: none; }
#sheet .fetch-goods { list-style: none; padding-left: 0; margin: 4px 0 0; }
#sheet .fetch-good { padding: 6px 0; border-bottom: 1px solid var(--parchment-2); }
#sheet .fetch-row { display: flex; flex-wrap: wrap; align-items: center; gap: 6px 8px; }
#sheet .fetch-row .switch { flex: 1 1 9em; width: auto; min-width: 0; gap: 8px; }
#sheet .fetch-row .switch .state { font-weight: 600; flex: none; }
#sheet .fetch-row .switch[aria-checked="false"] .state { color: var(--ink-soft, inherit); }
#sheet .fetch-row .stepper { display: flex; align-items: center; gap: 4px; flex: none; margin-left: auto; }
#sheet .fetch-row .stepper button { font-size: calc(20px * var(--ui-scale)); line-height: 1; padding: 0; }
#sheet .fetch-row .stepper .value { min-width: 4.8em; text-align: center; font-variant-numeric: tabular-nums; font-size: calc(14px * var(--ui-scale)); }
#sheet .fetch-line { margin: 4px 0 0; }
#sheet .fetch-note { display: flex; align-items: center; justify-content: space-between; gap: 8px; }
#sheet .fetch-note .fetch-line { flex: 1 1 0; min-width: 0; }
#sheet .fetch-note .show-me { flex: none; margin: 2px 0; }
#sheet .fetch-good .show-me { margin-top: 4px; }
#sheet .road-line { margin: 2px 0 0; font-weight: 600; }
#sheet .fetch-good .show-me { border: 1px solid var(--ink-line); }
/* Stage 4.7 (Task 7, ruling 469): the yearly tribute on the court sheet (src/ui/s47/pressure.js): the bar with its words beside it, a row per good with a mark
   (✓ shipped, never colour alone), the rules as wrapping lines, and Send as the court's choices are. Ruling 471: the house sheet's "Uses a month" row and the top
   tier's keep list. Ruling 470: the raid sheet's wealth line. */
#sheet .quota-title { margin: 4px 0; font-weight: 600; }
#sheet .quota-progress { display: flex; align-items: center; gap: 8px; margin: 4px 0; }
#sheet .quota-progress .meter { flex: 1 1 auto; width: auto; height: 10px; }
#sheet .quota-progress .quota-sent { flex: none; }
#sheet .quota-goods { list-style: none; padding-left: 0; margin: 4px 0; }
#sheet .quota-goods li { padding: 2px 0; }
#sheet .quota-goods .tick { color: var(--teal); font-weight: 700; }
#sheet .quota-goods .name { font-weight: 600; }
#sheet .quota-rule { margin: 4px 0; }
#sheet .quota-send { margin: 10px 0 4px; }
#sheet .quota-actions { margin: 4px 0 8px; }
#sheet .tier-use { align-items: baseline; gap: 8px; }
#sheet .tier-use > span:last-child { text-align: right; }
#sheet .raid-wealth { font-weight: 600; }

/* ── Version 5.0: City advice ── the scroll button between the numbers and the menu (a dot while something new is in the top items), and the sheet. */
#topbar .advice { position: relative; flex: none; display: inline-flex; align-items: center; justify-content: center; width: var(--tap); padding: 0; }
#topbar .advice .scroll { width: 22px; height: 22px; display: block; }
#topbar .advice .advice-dot { position: absolute; top: 5px; right: 5px; width: 11px; height: 11px; border-radius: 50%; background: var(--coral); box-shadow: 0 0 0 2px var(--parchment); }
#sheet .advice-list { list-style: none; padding-left: 0; margin: 6px 0; }
#sheet .advice-item { display: flex; align-items: flex-end; flex-wrap: wrap; gap: 6px 10px; padding: 8px 0; border-bottom: 1px solid var(--parchment-2); }
#sheet .advice-item .text { flex: 1 1 10em; display: flex; flex-direction: column; gap: 2px; min-width: 0; }
#sheet .advice-item .cost.short { color: var(--coral-text); font-weight: 600; }
#sheet .advice-item button { flex: none; }
#sheet .advice-well { font-weight: 600; margin: 4px 0; }
#sheet .advice-link { margin: 0 0 8px; padding-top: 0; }
/* Version 5.0 (Task 2): the next-step line at the top of a house sheet: its word first (Next step, Waiting, Locked, Ready, Kept, Empty: the word is the cue, the
   left edge only echoes it), the words wrapping, and Show me or View a 44 px target beside them or under them. */
#sheet .step-line { display: flex; align-items: center; flex-wrap: wrap; gap: 6px 10px; margin: 0 0 8px; padding: 6px 10px; background: var(--parchment-2); border-left: 4px solid var(--teal); border-radius: 6px; }
#sheet .step-line[data-reason='waiting'], #sheet .step-line[data-reason='locked'], #sheet .step-line[data-reason='empty'] { border-left-color: var(--amber-text); }
#sheet .step-line[data-reason='kept'], #sheet .step-line[data-reason='ready'] { border-left-color: var(--teal); }
#sheet .step-line .step-text { flex: 1 1 11em; min-width: 0; }
#sheet .step-line .step-label { margin-right: 2px; }
#sheet .step-line.short .step-text { color: var(--coral-text); }
#sheet .step-line button { flex: none; }
/* A phone's bar makes room for the advice button (a 44 px target beside the 44 px menu button): 4 px ends, the numbers run to the buttons, 3 px between
   them, and the menu button 44 px wide whatever the text size (its glyph grew with it). */
body:is([data-layout="portrait"], [data-layout="compact"]) #topbar { padding-left: max(4px, var(--safe-left)); padding-right: max(4px, var(--safe-right)); gap: 0; }
body:is([data-layout="portrait"], [data-layout="compact"])[data-text-size] #topbar .stats { column-gap: 3px; padding: 0; }
body:is([data-layout="portrait"], [data-layout="compact"]) #topbar .menu { padding: 0; width: var(--tap); }
body:is([data-layout="portrait"], [data-layout="compact"]) #topbar .advice { margin-right: 2px; }
body:is([data-layout="portrait"], [data-layout="compact"]) #topbar .stat { gap: 2px; }

/* Version 5.0 (Task 3, rulings 540-549): the legends of Home progress, Goods supply and Workers (ui/layersLegend.js). The name, the keys (each a swatch as the map draws it and a
   short label) and the x on one row that wraps among itself; Goods supply adds the good picker on a row of its own, five buttons at most, each 44 px at least, wrapping to a
   second row at large text. On a phone at 125% text or more the name and the keys' labels are for screen readers only (the strip's aria-label and each key's words still say
   them; the swatches show what the map shows), so the strip keeps the budget under a banner (tests/v50/layers/budget.test.js). */
#legend .strip.layer { flex-wrap: wrap; align-items: center; row-gap: 0; column-gap: 10px; max-width: 100%; box-sizing: border-box; }
#legend .strip.layer .layer-keys { display: flex; flex-wrap: wrap; align-items: center; gap: 0 10px; flex: 1 1 0; min-width: 0; }
#legend .strip.layer .layer-swatch { display: block; flex: 0 0 auto; }
#legend .strip.layer .key { gap: 4px; }
#legend .strip.layer .close { margin-left: auto; }
#legend .strip.layer .goods-pick { flex: 1 1 100%; display: flex; flex-wrap: wrap; align-items: center; gap: 4px; padding: 0 0 4px; }
#legend .strip.layer .goods-pick .good { min-width: var(--tap); min-height: var(--tap); padding: 2px 10px; font-size: calc(13px * var(--ui-scale)); line-height: 1.15; }
#legend .strip.layer .goods-pick .good[aria-pressed="true"] { background: var(--teal); color: var(--teal-ink); border-color: var(--teal); box-shadow: none; }
body:is([data-layout="portrait"], [data-layout="compact"]):is([data-text-size="125"], [data-text-size="150"]) #legend .strip.layer > strong.text,
body:is([data-layout="portrait"], [data-layout="compact"]):is([data-text-size="125"], [data-text-size="150"]) #legend .strip.layer .key .text { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; }
/* Goods supply on a phone: the picker is the strip's second row with the x at its end (the name is for screen readers: the Layers button says it and the picked good is pressed),
   so the strip is the keys' row and the picker's row, nothing taller (tests/v50/layers/budget.test.js: 375x667 under the court banner). */
body:is([data-layout="portrait"], [data-layout="compact"]) #legend .strip.layer.supply > strong.text { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; }
body:is([data-layout="portrait"], [data-layout="compact"]) #legend .strip.layer.supply { padding-top: 2px; padding-bottom: 2px; }
body:is([data-layout="portrait"], [data-layout="compact"]) #legend .strip.layer.supply .layer-keys { order: 1; flex: 1 1 100%; }
body:is([data-layout="portrait"], [data-layout="compact"]) #legend .strip.layer.supply .goods-pick { order: 2; flex: 1 1 0; min-width: 0; padding-bottom: 0; gap: 3px; }
body:is([data-layout="portrait"], [data-layout="compact"]) #legend .strip.layer.supply .goods-pick .good { padding: 2px 4px; }
body:is([data-layout="portrait"], [data-layout="compact"]) #legend .strip.layer.supply .close { order: 3; flex: 0 0 auto; margin-left: 0; align-self: flex-start; }
body:is([data-layout="portrait"], [data-layout="compact"]) #legend .strip.layer.supply .goods-pick .good { font-size: calc(13px * min(var(--ui-scale), 1.15)); }   /* at 150% the five goods stay one row beside the x (as the Done button's cap) */
body:is([data-layout="portrait"], [data-layout="compact"]) #legend .strip.layer.supply { column-gap: 2px; }
body:is([data-layout="portrait"], [data-layout="compact"]) #legend .strip.layer.supply .close { padding: 0; min-width: var(--tap); width: var(--tap); }
/* Held sideways (compact) the keys, the picker and the x share one row (the strip is wide enough at 100% text; at larger text the legend yields to the alert row, ruling 172). */
body[data-layout="compact"] #legend .strip.layer.supply { flex-wrap: nowrap; }
body[data-layout="compact"] #legend .strip.layer.supply .layer-keys { flex: 0 1 auto; }
body[data-layout="compact"] #legend .strip.layer.supply .goods-pick { flex: 1 1 auto; flex-wrap: nowrap; }

/* Version 5.0 (Task 4, rulings 550-559): drag areas and Build another. A garden or statue area's spacing is a row of its own at the foot of the confirm bar, three buttons that share
   the width (the one in force ticked, teal and aria-pressed: never colour alone), as the monument style row is; the toggle between one and an area is a 44 px icon beside Undo and
   the x; what Build another copies is one teal line (two at most on a phone) under the cost. On a sheet the button sits among the actions and its refusal is a coral line under them
   with its Show me. */
.confirm.has-area { flex-wrap: wrap; row-gap: 6px; }
.confirm .area-row { flex: 1 0 100%; order: 11; display: flex; flex-direction: column; gap: 4px; }
.confirm .area-choices { display: flex; flex-wrap: wrap; gap: 6px; }
.confirm .area-pick { flex: 1 1 auto; min-width: var(--tap); min-height: var(--tap); padding: 0 6px; font-size: calc(13px * min(var(--ui-scale), 1.15)); line-height: 1.2; white-space: nowrap; }
.confirm .area-pick[aria-pressed="true"] { background: var(--teal); color: var(--teal-ink); border-color: var(--teal); font-weight: 600; }
.confirm .area-toggle[aria-pressed="true"] { background: var(--teal); color: var(--teal-ink); border-color: var(--teal); }
.confirm .copy-line { font-size: calc(12px * var(--ui-scale)); }
body:is([data-layout="portrait"], [data-layout="compact"]) .confirm .copy-line { display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; }
#sheet .another-note { margin: 6px 0 0; display: flex; flex-wrap: wrap; align-items: center; gap: 4px 8px; }
#sheet .another-note .show-me { min-height: var(--tap); }

/* Version 5.0 (Task 5): the road bar's Route / Straight toggle, a row of its own under the bar's words and buttons, as the area bar's spacing row is. */
.confirm.has-route { flex-wrap: wrap; row-gap: 6px; }
.confirm .route-row { flex: 1 0 100%; order: 12; display: flex; flex-direction: column; gap: 4px; }
.confirm .route-choices { display: flex; flex-wrap: wrap; gap: 6px; }
.confirm .route-pick { flex: 1 1 auto; min-width: var(--tap); min-height: var(--tap); padding: 0 6px; font-size: calc(13px * min(var(--ui-scale), 1.15)); line-height: 1.2; white-space: nowrap; }
.confirm .route-pick[aria-pressed="true"] { background: var(--teal); color: var(--teal-ink); border-color: var(--teal); font-weight: 600; }
