/* Allergy ID card pages — shared stylesheet.
 *
 * Was CARD_PAGE_STYLE, a 1,145-line template literal in src/index.js
 * interpolated into <style> on four pages. Moved out under the [assets]
 * pipeline (see wrangler.toml): it is now a real .css file, cached for 24h and
 * shared across those four pages, where before it was re-sent inline on every
 * single page load — the HTML is Cache-Control: no-store, so none of it was
 * ever cacheable.
 *
 * BRAND VALUES LIVE IN src/brand.js. Every one of them enters this file in the
 * :root block below and nowhere else. test_card_page_css.mjs asserts each token
 * still equals the constant it came from, so changing a brand colour in
 * brand.js fails the suite here rather than silently leaving this file stale.
 * If that test fails, update the :root block; do not edit values further down.
 *
 * MOTION IS THE ONE BRAND VALUE THAT DOES NOT ENTER VIA :root, DELIBERATELY.
 * Every curve here is written var(--motion-entrance, cubic-bezier(.16,1,.3,1))
 * — the name from BRAND.motion.ease with its value inline as the fallback.
 *
 * A var() with a fallback is valid whether or not any page defines the
 * property, so this file cannot be broken by a page that links it and forgets
 * a :root line. That matters more here than for the colours above: an
 * undefined custom property inside a shorthand is invalid at computed-value
 * time, which drops the WHOLE transition declaration rather than falling back
 * — the motion silently stops existing while the CSS still reads as correct.
 * None of the four pages linking this file ships MOTION_CSS_VARS today, so
 * every one of these resolves to its fallback, and that is the intended
 * steady state rather than an omission.
 *
 * The name is not decoration: it says which of the four gestures a site is,
 * so the next person editing it knows what to reach for. And the fallbacks
 * cannot drift — test_motion_scale.mjs asserts each one still equals the
 * constant it was copied from, the same guarantee the :root block has above.
 */

:root{--pine:#0e2e24;--pine-dark:#071a14;--cream:#f6f1e4;--paper:#fffdf7;--safe:#3d7a4e;--safe-bg:#e7f2e8;--caution:#c1502e;--caution-bg:#fbe9e2;--trace:#8a6416;--trace-bg:#faf1de;--unknown:#4a6a8a;--unknown-bg:#e9eff4;--line:#d8cfb8;--ink:#1c332b;--ink-soft:#5b6a61;--pine-light:#1d5340;--card-font-body:'Lato',-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Helvetica,Arial,sans-serif;}
/* The shared "Report a problem" FAB (see BUG_REPORT_WIDGET_HTML) now lives
   as a slim vertical tab pinned to the right edge, vertically centered —
   no longer anchored to the bottom of the viewport, so it no longer risks
   permanently overlapping the last bit of content on a short page the way
   the old bottom-left pill did. A little bottom padding is kept anyway so
   the page never ends flush with the very bottom edge of the screen. */
*{box-sizing:border-box;} body{margin:0;background:var(--pine);color:var(--ink);font-family:var(--card-font-body);display:flex;justify-content:center;padding-bottom:60px;}
::selection{background:var(--pine);color:var(--cream);}
:focus-visible{outline:2px solid var(--pine);outline-offset:2px;}
.app{width:100%;max-width:480px;background:var(--pine);box-shadow:0 8px 30px rgba(0,0,0,0.25);min-height:100vh;}
/* Hasib: "want the card more premium, expensive and slick" (extended to
   "not just the card but the rest of the dashboard" — meaning this page's
   own chrome around the card, not the separate restaurant/institution
   dashboards). Same visual language as the id-card itself: a top bevel
   highlight for a light-catching edge, deepened elevation shadow for more
   presence. Restated in full across the severity-ring variants below —
   those used to REPLACE box-shadow wholesale (header.severity-ring-1 has
   higher specificity than plain header), which was silently dropping the
   header's own elevation shadow whenever a ring was showing; fixed here
   as part of the same pass, not just the new bevel line. */
header{text-align:center;color:var(--cream);padding:30px 24px 24px;background:radial-gradient(circle at 22% -12%,var(--pine-light) 0%,var(--pine) 45%,var(--pine-dark) 100%);box-shadow:0 3px 20px rgba(0,0,0,0.28),inset 0 1px 0 rgba(255,255,255,0.28);border-bottom:1px solid rgba(255,255,255,0.14);position:relative;}
header .mark{width:52px;height:52px;margin:0 auto 14px;padding-top:2px;background:var(--cream);border-radius:50%;display:flex;align-items:center;justify-content:center;font-size:24px;box-shadow:0 4px 14px rgba(0,0,0,0.35),inset 0 0 0 1px rgba(255,255,255,0.45),inset 0 -2px 3px rgba(0,0,0,0.08);}
/* Severity ring — Hasib, with a screenshot of the orange ring showing at
   the top of the card page: "remove this from the card border and from
   the top" / "i mean remove the orange from the border." Retired rather
   than deleted, in case a differently-styled severity indicator is wanted
   later. See severityRingTier in allergyCardViewPage for how the tier
   itself used to be decided (still computed there for reference, just no
   longer applied to any element).
header.severity-ring-1{box-shadow:0 3px 20px rgba(0,0,0,0.28),inset 0 1px 0 rgba(255,255,255,0.28),inset 0 0 0 3px var(--caution);}
header.severity-ring-2{box-shadow:0 3px 20px rgba(0,0,0,0.28),inset 0 1px 0 rgba(255,255,255,0.28),inset 0 0 0 5px var(--caution);}
header.severity-ring-3{box-shadow:0 3px 20px rgba(0,0,0,0.28),inset 0 1px 0 rgba(255,255,255,0.28),inset 0 0 0 7px var(--caution);} */
header h1{font-size:19px;margin:0 0 6px;}
header p{font-size:13px;margin:0;color:#dfe9e2;}
/* Hasib: "on the id card, make the powered by allergy spotter color the same
   as the rest" — POWERED_BY_BADGE_HTML's shared style hardcodes a flat
   #0a3226 box (needed for self-contained contrast on the ~30 other pages
   that use it against a light cream background), but here it sits directly
   on this page's own dark .app/header green (var(--pine)) right below the
   header, so the badge's slightly different green read as a mismatched
   patch rather than blending in. Two-class selector so this wins over the
   badge's own single-class rule regardless of source order (that inline
   <style> renders later in the body, after this page-head stylesheet, so
   equal specificity would otherwise let it win on source order alone). */
.app .powered-by-badge{background:var(--pine);}
section{background:var(--paper);padding:22px;}
.chip-grid{display:flex;flex-wrap:wrap;gap:8px;margin:14px 0;}
.chip{border:1.5px solid var(--line);background:var(--cream);color:var(--ink);border-radius:999px;padding:8px 13px;font-size:13px;cursor:pointer;font-family:inherit;transition:background-color 0.15s var(--motion-standard, cubic-bezier(.2,.75,.25,1)),color 0.15s var(--motion-standard, cubic-bezier(.2,.75,.25,1)),border-color 0.15s var(--motion-standard, cubic-bezier(.2,.75,.25,1));}
.chip.active{background:var(--pine);color:var(--cream);border-color:var(--pine);}
input[type=text]{width:100%;padding:10px 12px;border:1.5px solid var(--line);border-radius:8px;font-size:14px;font-family:inherit;margin-top:6px;}
button.primary{width:100%;padding:13px;margin-top:18px;border:none;border-radius:10px;background:var(--pine);color:var(--cream);font-size:15px;font-weight:700;cursor:pointer;font-family:inherit;}
button.primary:disabled{opacity:0.55;cursor:default;}
button.primary:active{transform:scale(0.99);}
button.secondary,a.secondary{width:100%;padding:11px;margin-top:10px;border:1.5px solid var(--pine);border-radius:10px;background:var(--paper);color:var(--pine);font-size:13.5px;font-weight:600;cursor:pointer;font-family:inherit;box-sizing:border-box;}
.voice-input-btn{width:auto !important;flex:0 0 auto;margin-top:0 !important;padding:10px 13px !important;font-size:15px !important;}
.voice-input-btn.listening{background:var(--caution,#c1502e) !important;color:#fff !important;border-color:var(--caution,#c1502e) !important;animation:voiceListeningPulse 1s ease-in-out infinite;}
@keyframes voiceListeningPulse{0%,100%{opacity:1;}50%{opacity:0.55;}}
@media (prefers-reduced-motion: reduce){.voice-input-btn.listening{animation:none;}}
.voice-setup-box{background:var(--paper,#fffdf7);border:1.5px solid var(--pine,#1c332b);border-radius:12px;padding:14px;margin:14px 0;text-align:center;}
.voice-setup-btn{width:100%;font-size:15px;}
.voice-setup-btn.listening{background:var(--caution,#c1502e) !important;border-color:var(--caution,#c1502e) !important;animation:voiceListeningPulse 1s ease-in-out infinite;}
@media (prefers-reduced-motion: reduce){.voice-setup-btn.listening{animation:none;}}
.voice-setup-reset-btn{width:100%;font-size:13px;margin-top:6px;padding:6px 10px;}
.clear-fields-btn{width:100%;font-size:13px;margin-top:8px;padding:8px 10px;opacity:0.85;}
.voice-fill-summary{text-align:left;font-size:12.5px;background:var(--cream,#f6f1e4);border-radius:8px;padding:8px 10px;margin-top:8px;line-height:1.5;}
.voice-fill-summary strong{color:var(--pine,#1c332b);}
.voice-fill-summary div{margin-bottom:2px;}
.voice-fill-summary div:last-child{margin-bottom:0;}
.voice-fill-note{font-size:11.5px;color:var(--pine,#1c332b);background:var(--cream,#f6f1e4);border-radius:6px;padding:4px 8px;margin-top:4px;display:inline-block;}
.voice-confirm-flag{display:flex;align-items:center;gap:6px;font-size:12px;color:var(--caution,#c1502e);font-weight:600;margin-top:4px;padding:6px 8px;background:var(--caution-bg,#fbeee7);border-radius:6px;border:1px solid var(--caution,#c1502e);}
.voice-confirm-flag button{width:auto;margin:0;padding:4px 10px;font-size:12px;background:var(--caution,#c1502e);color:#fff;border:none;border-radius:5px;cursor:pointer;}
.voice-confirm-flag.confirmed{display:none;}
.voice-live-captions{min-height:16px;font-size:13px;font-style:italic;color:var(--pine,#1c332b);opacity:0.8;margin:6px 0 0;padding:4px 8px;background:var(--cream,#f6f1e4);border-radius:6px;text-align:left;}
.chip.voice-unsure{border:1.5px dashed var(--caution,#c1502e) !important;}
.note{font-size:12.5px;color:var(--ink-soft);line-height:1.5;margin:10px 0;}
.warn-box{background:var(--trace-bg);color:var(--trace);border:1.5px solid var(--trace);border-radius:10px;padding:12px 14px;font-size:12.5px;line-height:1.5;margin:14px 0;}
.link-box{background:var(--cream);border:1.5px solid var(--line);border-radius:10px;padding:10px 12px;font-size:12.5px;word-break:break-all;margin:8px 0;}
/* Recall alert: uses the same --caution colours as the true 999 emergency
   box below (a matching allergen recall is a genuine, time-sensitive
   safety concern, not a minor nudge), but is visually and textually
   distinct from it — its own heading ("Recall alert", never "Emergency"),
   a Dismiss control (the 999 box has none, deliberately, since it should
   never be dismissable), and it sits above the ID card rather than below
   it, so it's seen but never confused for "call 999 now".
   Given a premium pass (user feedback: "make them shiny, glossy, precise,
   premium, neat and elegant"): a subtle radial gradient and a proper
   colour-matched elevation shadow instead of the old flat tint + hairline
   shadow, a bevelled icon badge instead of a flat circle, and a solid
   hairline divider instead of a dashed one. Deliberately NO shine-sweep or
   other flourish animation here though, unlike the id-card/QR/wallet
   button — this is a safety warning, not a moment to celebrate, so the
   polish is all in the material (colour, depth, precision), never motion. */
.recall-alert-banner{background:radial-gradient(circle at 12% -15%,#fef6f2 0%,var(--caution-bg) 55%,#f6ddd0 100%);color:var(--caution);border:1.5px solid var(--caution);border-radius:14px;padding:16px;line-height:1.5;margin:0 0 14px;box-shadow:0 10px 22px rgba(193,80,46,0.16),0 2px 6px rgba(193,80,46,0.1);}
.recall-alert-head{display:flex;align-items:flex-start;gap:10px;margin:0 0 10px;}
.recall-alert-icon{flex-shrink:0;width:28px;height:28px;border-radius:50%;background:radial-gradient(circle at 35% 30%,#d8623d,var(--caution) 75%);color:var(--paper);display:flex;align-items:center;justify-content:center;font-size:15px;font-weight:800;box-shadow:inset 0 0 0 1px rgba(255,255,255,0.3),0 2px 4px rgba(0,0,0,0.18);}
.recall-alert-title{font-weight:800;font-size:14.5px;margin:0;}
/* The plain-language "why am I seeing this" line — the title alone named
   the matching allergen already, but didn't explain the mechanism (that
   this is automatic, matched against the guest's own card, not a generic
   feed of every recall going). Spelling that out here is what makes this
   feel like a feature working on the guest's behalf rather than a random
   pop-up. */
.recall-alert-why{font-size:12.5px;font-weight:600;margin:3px 0 0;opacity:.85;}
.recall-alert-body{font-size:12.5px;color:var(--ink);background:var(--paper);border-left:3px solid var(--caution);border-radius:0 8px 8px 0;padding:10px 12px;margin:0 0 12px;box-shadow:0 1px 3px rgba(0,0,0,0.05);}
.recall-alert-meta{font-size:12.5px;display:flex;align-items:center;justify-content:space-between;gap:10px;flex-wrap:wrap;padding-top:10px;border-top:1px solid rgba(193,80,46,0.32);}
.recall-alert-meta a{color:inherit;font-weight:700;}
.recall-alert-dismiss{background:var(--paper);border:1px solid var(--caution);color:var(--caution);border-radius:999px;padding:5px 12px;font-size:11px;font-weight:700;cursor:pointer;font-family:inherit;box-shadow:0 1px 2px rgba(0,0,0,0.04);transition:background-color .15s var(--motion-standard, cubic-bezier(.2,.75,.25,1)),color .15s var(--motion-standard, cubic-bezier(.2,.75,.25,1));}
.recall-alert-dismiss:hover{background:var(--caution);color:var(--paper);}
/* User feedback: showing the recall inline (above) pushed the rest of the
   card's own content down, reading as clutter on a card that should stay
   focused on the guest's own allergens. Moved to a slim tab pinned to the
   edge of the screen instead — same pattern as the "Report a bug" tab
   (see BUG_REPORT_WIDGET_HTML) — hidden entirely until there's actually a
   matching recall, and opening into an overlay on tap rather than sitting
   in the page's normal scroll flow. Pinned to the LEFT edge specifically so
   it never collides with the bug-report tab already on the right. */
.recall-tab{position:fixed;top:58%;left:0;transform:translateY(-50%);display:flex;flex-direction:column;align-items:center;gap:6px;padding:12px 7px;border:none;border-radius:0 10px 10px 0;background:radial-gradient(circle at 30% 20%,#dd6a41,var(--caution) 75%);color:#fff;cursor:pointer;box-shadow:3px 3px 12px rgba(0,0,0,0.25),inset 0 0 0 1px rgba(255,255,255,0.18);z-index:9996;font-family:var(--card-font-body);}
.recall-tab-count{background:#fff;color:var(--caution);border-radius:999px;min-width:16px;height:16px;font-size:10px;font-weight:800;display:flex;align-items:center;justify-content:center;padding:0 3px;}
.recall-overlay{display:none;position:fixed;inset:0;background:rgba(20,30,25,0.72);z-index:9997;align-items:flex-start;justify-content:center;padding:60px 20px 20px;box-sizing:border-box;overflow-y:auto;}
.recall-overlay-inner{max-width:380px;width:100%;}
.recall-overlay-close{display:block;width:100%;margin-top:4px;background:transparent;border:1.5px solid rgba(255,255,255,0.6);border-radius:999px;padding:9px 20px;font-size:12.5px;font-weight:700;color:#fff;cursor:pointer;font-family:inherit;}
/* Task #12: "In an emergency, call 999" — deliberately the loudest, most
   urgent-looking box on the whole card (--caution, not --trace like
   warn-box above), and always shown first, above the charity helplines,
   so nobody ever mistakes a charity's non-emergency number for the one to
   call in a genuine reaction. */
/* Hasib: "what is this, is this needed if it is on emergency button" — the
   old .emergency-box was a bordered, three-line card repeating info the
   Emergency button's overlay already shows better (tappable "Call 999",
   tappable "Call [contact]", the EpiPen instruction, DOB/address for the
   ambulance crew). That first trim dropped the 999 repeat but kept a
   one-line EpiPen/contact summary under the button. Hasib later asked "do
   we need [the contact name] there, as all that detail is in the emergency
   button" — so even that single line was still a duplicate of what the
   overlay already shows, and it was contributing to the page reading as a
   plain dashboard right above the premium id-card. Removed outright, CSS
   and all — the button now leads straight into the id-card below. */
/* Task #13: charity/regulator resource links — plain list, not styled as
   urgent (only the 999 box above gets that treatment), so Anaphylaxis UK's
   own helpline is never confused for an emergency line — its own
   description text says so explicitly too. */
.resource-links{margin:14px 0;padding:0;list-style:none;}
.resource-links li{padding:10px 12px;border-radius:8px;background:var(--cream);margin-bottom:6px;font-size:12.5px;line-height:1.5;}
.resource-links a{color:var(--pine);font-weight:700;text-decoration:none;}
.resource-links a:hover{text-decoration:underline;}
.resource-links .resource-desc{display:block;color:var(--ink-soft);font-weight:400;margin-top:2px;}
.success-banner{display:flex;align-items:center;justify-content:center;gap:8px;background:var(--safe-bg,#eef6ef);color:var(--safe,#3d7a4e);border-radius:10px;padding:12px 14px;font-size:13.5px;font-weight:700;margin:0 0 14px;}
.success-check{width:22px;height:22px;flex-shrink:0;}
.success-check circle{stroke:var(--safe,#3d7a4e);stroke-width:3;fill:none;stroke-dasharray:166;stroke-dashoffset:166;animation:successCheckCircle 0.45s var(--motion-entrance, cubic-bezier(.16,1,.3,1)) forwards;}
.success-check path{stroke:var(--safe,#3d7a4e);stroke-width:4;fill:none;stroke-dasharray:48;stroke-dashoffset:48;animation:successCheckMark 0.25s var(--motion-entrance, cubic-bezier(.16,1,.3,1)) forwards 0.4s;}
@keyframes successCheckCircle{to{stroke-dashoffset:0;}}
@keyframes successCheckMark{to{stroke-dashoffset:0;}}
@media (prefers-reduced-motion: reduce){.success-check circle,.success-check path{animation:none;stroke-dashoffset:0;}}
/* "Here's what else your card can carry" — the one-time disclosure shown
   directly under the success banner on a card that was just created (see
   cardExtras in allergyCardViewPage). Deliberately styled as an OFFER and not
   a warning: the safe/cream palette the card uses for good news, not the
   .warn-box amber directly above it, which on this page means "you need to act
   on this". The person has just done something good for their own safety. */
.card-extras{position:relative;background:var(--paper);border:1.5px solid var(--line);border-radius:12px;padding:16px 18px 14px;margin:0 0 16px;}
.card-extras-title{font-size:15px;font-weight:700;color:var(--ink);margin:0 6px 6px 0;line-height:1.35;}
.card-extras-lede{font-size:12.5px;color:var(--ink-soft);line-height:1.55;margin:0 0 12px;}
.card-extras-list{list-style:none;margin:0;padding:0;display:flex;flex-direction:column;gap:8px;}
.card-extras-item{margin:0;}
/* A whole-row button rather than a link with a separate tap target: this is
   read on a phone, often one-handed, and 44px is the floor everywhere else on
   this page. */
.card-extras-open{display:block;width:100%;text-align:left;background:var(--cream);border:1px solid var(--line);border-radius:10px;padding:10px 12px;font-family:inherit;cursor:pointer;min-height:44px;transition:background-color 0.15s var(--motion-standard, cubic-bezier(.2,.75,.25,1)),border-color 0.15s var(--motion-standard, cubic-bezier(.2,.75,.25,1));}
.card-extras-open:hover{background:#efe6cf;border-color:var(--pine);}
.card-extras-open:focus-visible{outline:2px solid var(--pine);outline-offset:2px;}
.card-extras-item-title{display:block;font-size:13.5px;font-weight:700;color:var(--pine);}
.card-extras-item-body{display:block;font-size:12.5px;color:var(--ink-soft);line-height:1.5;margin-top:2px;}
.card-extras-foot{font-size:12px;color:var(--ink-soft);margin:12px 0 0;line-height:1.5;}
.card-extras-dismiss{position:absolute;top:6px;right:6px;width:32px;height:32px;display:flex;align-items:center;justify-content:center;background:none;border:none;color:var(--ink-soft);font-size:20px;line-height:1;cursor:pointer;border-radius:8px;padding:0;}
.card-extras-dismiss:hover{background:rgba(0,0,0,0.05);color:var(--ink);}
.card-extras-dismiss:focus-visible{outline:2px solid var(--pine);outline-offset:2px;}
@media (prefers-reduced-motion: reduce){.card-extras-open{transition:none;}}
.allergen-list{margin:14px 0;padding:0;list-style:none;}
.allergen-list li{padding:9px 12px;border-radius:8px;background:var(--cream);margin-bottom:6px;font-size:14px;}
/* The actual "wallet card" visual — this is what a guest's saved allergens
   are shown in now, instead of a plain bulleted list, so the "ID card"
   name/metaphor this feature is built around actually looks like one.
   Deliberately its own dark, brand-coloured surface sitting on the page's
   light --paper section, echoing a real card resting on a counter. */
/* Hasib: "a very faint noise/grain texture... would make it feel like a
   genuine material rather than a CSS gradient." A tiny seeded feTurbulence
   SVG, tiled at 140px and layered UNDER the existing radial gradient via a
   second background-image (backgrounds stack in the order listed, first on
   top) — so it reads as the card's own material grain, with the gradient's
   colour and the ::before/::after sheen/shine layers (below) still painting
   on top of it exactly as before. Opacity is baked into the SVG itself
   (0.045) rather than via mix-blend-mode, so it renders identically and
   predictably across browsers rather than depending on blend-mode support. */
/* Hasib: "is it a square or a rectangle, isn't rectangle better" — it had
   no fixed shape at all, just content-driven height, which read square-ish
   for anyone with only 1-2 allergens. "keep the height flexible but nudge
   the proportions wider... shorter minimum height... reads more like a
   card." -> "is this better" -> "yes". Tightened the internal vertical
   spacing below (top row/name/tags margins, footer padding) rather than
   fixing a strict credit-card ratio — a hard rectangle would fight the
   density-tier chip wrapping built for guests with many allergens (up to
   14), the exact cramping problem that fix solved. This keeps it flexible
   for a long allergen list while reading noticeably wider-than-tall for a
   short one, without capping anyone's real content.
   Hasib, with a screenshot of the QR/back face: "fix the formatting and
   spacing immediately for laptops and phones, it is not good, shocking,
   fix it" — that tightening above shrank the card's height for anyone
   with a short allergen list, but the flip-BACK face has its own fixed
   content (a 180px QR block + label + badge) that doesn't shrink with the
   allergen count. Once the container got shorter than that fixed content,
   .id-card's own overflow:hidden started clipping/cramming the back face
   — QR, "Show this to staff" and "Tap to flip back" squashed together or
   cut off. Fixed with a min-height here (both faces share this base rule)
   sized to comfortably fit the back's content even at its tightened
   sizing below, so the back never has less room than it needs regardless
   of how short the front's allergen list is. Also switched to flex-column
   so the front face can pin its footer/badge to the bottom (margin-top:
   auto on .id-card-footer below) instead of leaving one big empty gap
   after the tags on a short card — extra height now distributes as
   intentional breathing room above the footer, the way a real ID card's
   layout holds together at any content length. */
.id-card{position:relative;overflow:hidden;border-radius:18px;padding:20px 20px 16px;margin:0;display:flex;flex-direction:column;min-height:290px;
  background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='140' height='140'%3E%3Cfilter id='g'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='2' stitchTiles='stitch'/%3E%3CfeColorMatrix type='saturate' values='0'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23g)' opacity='0.045'/%3E%3C/svg%3E"),radial-gradient(circle at 20% 12%,var(--pine-light) 0%,var(--pine) 42%,var(--pine-dark) 100%);
  background-repeat:repeat,no-repeat;
  background-size:140px 140px,auto;
  color:var(--cream);
  border:1px solid rgba(255,255,255,0.16);
  /* Hasib: "bevelled top edge — a thin light-catching line along the
     card's top edge... the way a metal card's edge catches light." A
     1px inset highlight right at the top, on top of (not instead of) the
     existing elevation shadow — repeated in each severity-ring variant
     below since box-shadow can't be layered additively across separate
     rules, only replaced wholesale.
     Hasib later: "want the card more premium, expensive and slick" — added
     a matching dark line along the BOTTOM edge too (inset 0 -1px), so the
     card reads as a genuinely extruded object catching light from above
     rather than just having one bright line floating on top — real metal
     edges have both the highlight AND the shadow that falls away from it.
     Border opacity bumped too (0.08→0.16) for a crisper, more considered
     edge instead of one that nearly disappears against the gradient. */
  box-shadow:0 16px 34px rgba(0,0,0,0.38),0 3px 9px rgba(0,0,0,0.24),inset 0 1px 0 rgba(255,255,255,0.35),inset 0 -1px 0 rgba(0,0,0,0.25);
  animation:idCardIn 0.45s var(--motion-entrance, cubic-bezier(.16,1,.3,1)) backwards;}
/* Severity ring — retired, see the matching comment above header's
   severity-ring rules for the Hasib quote. Same box-shadow stack as
   .id-card's base rule, minus the var(--caution) ring layer.
.id-card.severity-ring-1{box-shadow:0 16px 34px rgba(0,0,0,0.38),0 3px 9px rgba(0,0,0,0.24),inset 0 1px 0 rgba(255,255,255,0.35),inset 0 -1px 0 rgba(0,0,0,0.25),inset 0 0 0 3px var(--caution);}
.id-card.severity-ring-2{box-shadow:0 16px 34px rgba(0,0,0,0.38),0 3px 9px rgba(0,0,0,0.24),inset 0 1px 0 rgba(255,255,255,0.35),inset 0 -1px 0 rgba(0,0,0,0.25),inset 0 0 0 5px var(--caution);}
.id-card.severity-ring-3{box-shadow:0 16px 34px rgba(0,0,0,0.38),0 3px 9px rgba(0,0,0,0.24),inset 0 1px 0 rgba(255,255,255,0.35),inset 0 -1px 0 rgba(0,0,0,0.25),inset 0 0 0 7px var(--caution);} */
@keyframes idCardIn{from{opacity:0;transform:scale(0.94) translateY(6px);}to{opacity:1;transform:scale(1) translateY(0);}}
@media (prefers-reduced-motion: reduce){.id-card{animation:none;}}
.id-card::before{content:'';position:absolute;inset:0;pointer-events:none;
  background:linear-gradient(120deg,rgba(255,255,255,0.10) 0%,rgba(255,255,255,0) 45%);}
/* One-time diagonal shine sweep as the card settles into place — plays
   once, shortly after the pop-in above finishes, then stays gone (not a
   looping/decorative effect, so it doesn't get tiresome on the 50th time
   a guest opens their own card). */
.id-card::after{content:'';position:absolute;top:0;left:-60%;width:35%;height:100%;
  background:linear-gradient(115deg,transparent,rgba(255,255,255,0.3),transparent);
  transform:skewX(-20deg);animation:idCardShine 0.9s var(--motion-entrance, cubic-bezier(.16,1,.3,1)) 0.35s 1;pointer-events:none;}
@keyframes idCardShine{from{left:-60%;}to{left:130%;}}
@media (prefers-reduced-motion: reduce){.id-card::after{display:none;}}
/* Hasib: "loyalty/tenure patina — a subtle visual cue that a card's been
   active and verified-scanned for a while... signaling trust without
   another badge to explain." Deliberately no new text/badge anywhere —
   just a faint warm hairline (the same gold already used for the
   Powered-by badge and other "this is the premium/trusted bit" moments
   elsewhere in the app) added to the card's existing box-shadow stack,
   which grows very slightly more defined the longer the card's been
   around. Same "box-shadow can't be layered additively, only replaced
   wholesale" constraint as the old severity-ring variants above, so each
   tier repeats the full base stack rather than adding to it. A brand-new
   card (tenureTier 'new') gets neither class and renders byte-for-byte as
   before this build. */
.id-card.id-card-patina-established{box-shadow:0 16px 34px rgba(0,0,0,0.38),0 3px 9px rgba(0,0,0,0.24),inset 0 1px 0 rgba(255,255,255,0.35),inset 0 -1px 0 rgba(0,0,0,0.25),inset 0 0 0 1px rgba(234,196,122,0.16);}
.id-card.id-card-patina-trusted{box-shadow:0 16px 34px rgba(0,0,0,0.38),0 3px 9px rgba(0,0,0,0.24),inset 0 1px 0 rgba(255,255,255,0.35),inset 0 -1px 0 rgba(0,0,0,0.25),inset 0 0 0 1.5px rgba(234,196,122,0.3),0 0 24px rgba(234,196,122,0.16);}
.id-card-top{display:flex;align-items:flex-start;justify-content:space-between;margin-bottom:16px;position:relative;}
.id-card-brand-lockup{display:flex;align-items:center;gap:9px;}
.id-card-chip{width:34px;height:34px;flex-shrink:0;padding-top:2px;border-radius:50%;background:rgba(255,255,255,0.16);display:flex;align-items:center;justify-content:center;font-size:17px;line-height:1;box-shadow:inset 0 0 0 1px rgba(255,255,255,0.22);}
.id-card-label{font-size:10px;letter-spacing:.16em;text-transform:uppercase;opacity:.8;font-weight:700;text-shadow:-0.5px -0.5px 0 rgba(0,0,0,0.3);}
/* Hasib: "embossed/debossed lettering — give the guest's name and card
   code a subtle inset text-shadow so it reads like lettering pressed
   into metal." Classic deboss trick: a dark shadow on the up-light side
   (top-left) reading as the recess's own shadow, plus a faint light
   catch on the opposite side (bottom-right) reading as the recess's far
   wall — kept alongside the name's existing soft blurred shadow (still
   needed for legibility against the grain/gradient background) rather
   than replacing it. */
.id-card-name{font-size:20px;font-weight:800;margin:0 0 10px;position:relative;letter-spacing:.015em;text-shadow:0 1px 3px rgba(0,0,0,0.28),-1px -1px 0 rgba(0,0,0,0.35),1px 1px 0 rgba(255,255,255,0.15);}
/* Hasib: "personalized typography — letting the cardholder pick a name font
   (a script option alongside the standard one) for a more personal, less
   clinical feel." Bumped up in size and given a touch more letter-spacing
   since script faces read smaller/tighter than the bold sans default at the
   same declared size — this keeps the two options feeling similarly
   prominent rather than the script one looking like an afterthought. */
.id-card-name-script{font-family:'Dancing Script',cursive;font-weight:700;font-size:27px;letter-spacing:.01em;}
/* "How this card looks" manage-form toggle for the above — two equal-weight
   buttons rather than a plain radio list, since this is a visual choice best
   shown rendered, not just named. */
.name-font-choice-row{display:flex;gap:8px;}
.name-font-choice{position:relative;flex:1;display:flex;align-items:center;justify-content:center;gap:0;padding:10px 12px;border:1.5px solid var(--line);border-radius:10px;cursor:pointer;transition:border-color .15s,background .15s,box-shadow .15s;}
.name-font-choice input{position:absolute;opacity:0;pointer-events:none;}
/* Hasib: "when you click script, it doesn't show clearly you have selected
   it" — the old active state was a 1.5px border tint plus a background at
   6% opacity, easy to miss at a glance. Now a heavier border + a visibly
   tinted fill + an inset ring, PLUS an explicit checkmark badge in the
   corner, so which option is selected is unambiguous even in a quick
   glance, not just a subtle color shift. */
.name-font-choice.active{border-color:var(--pine);background:rgba(45,90,61,0.1);box-shadow:inset 0 0 0 1px var(--pine);}
.name-font-choice.active::after{content:'✓';position:absolute;top:-8px;right:-8px;width:18px;height:18px;border-radius:50%;background:var(--pine);color:#fff;font-size:11px;font-weight:700;line-height:18px;text-align:center;box-shadow:0 1px 3px rgba(0,0,0,0.3);}
.name-font-choice-label{font-size:14px;font-weight:600;}
.id-card-tags{display:flex;flex-wrap:wrap;gap:7px;margin:0 0 12px;position:relative;}
/* Hasib: "is this premium enough, or can it be presented better" — the pills
   read a little flat/outline-y at low fill opacity, and the loose spacing
   above/below them left the card feeling sparse relative to how much detail
   is going on in the background texture. Deepened the fill and border so
   each pill has more presence on its own, tightened the padding/letter
   spacing to match, and pulled the name->tags->footer rhythm in a touch
   (13px/16px -> 10px/12px) so the block reads denser and more deliberate. */
.id-card-tag{background:rgba(255,255,255,0.2);border:1px solid rgba(255,255,255,0.34);border-radius:999px;padding:6px 12px;font-size:13px;font-weight:600;letter-spacing:.01em;box-shadow:inset 0 1px 0 rgba(255,255,255,0.2);
  animation:idCardTagIn 0.35s var(--motion-entrance, cubic-bezier(.16,1,.3,1)) backwards;}
.id-card-tag:nth-child(1){animation-delay:.08s;}
.id-card-tag:nth-child(2){animation-delay:.15s;}
.id-card-tag:nth-child(3){animation-delay:.22s;}
.id-card-tag:nth-child(4){animation-delay:.29s;}
.id-card-tag:nth-child(n+5){animation-delay:.34s;}
@keyframes idCardTagIn{from{opacity:0;transform:translateY(4px);}to{opacity:1;transform:translateY(0);}}
@media (prefers-reduced-motion: reduce){.id-card-tag{animation:none;}}
/* Hasib, with a screenshot of a card carrying ~14 allergens: "if i group
   many allergies, it looks like this, is the card then horrible" — with
   the default chip size, a long list wraps into an uneven, cramped grid,
   and a long official label (e.g. "Sulphur Dioxide and Sulphites") can
   wrap mid-chip. Rather than truncating anyone's real allergens or capping
   the list, the chips themselves quietly scale down once there are enough
   of them, so a long list still reads as one tidy block instead of a messy
   pile — same information, calmer shape. */
.id-card-tags.id-card-tags-compact{gap:6px;}
.id-card-tags.id-card-tags-compact .id-card-tag{padding:6px 11px;font-size:12px;}
.id-card-tags.id-card-tags-dense{gap:5px;}
.id-card-tags.id-card-tags-dense .id-card-tag{padding:5px 9px;font-size:11px;letter-spacing:-.005em;}
.id-card-empty{font-size:12.5px;opacity:.75;position:relative;text-align:center;padding:10px 0;}
.id-card-empty-icon{display:block;font-size:24px;margin-bottom:4px;}
/* Hasib: "should something come on the card that this is a severe allergy,
   like a key" — a first-time viewer (restaurant staff, a relief carer) has
   no way to know what the ⚠️ prefix on a tag means without this. Kept to a
   single quiet caption line, only rendered when a severe tag is actually
   present, matching the same restrained opacity/tracking as the footer
   rather than a bright callout. */
.id-card-severe-key{font-size:10.5px;opacity:.68;letter-spacing:.04em;margin:-10px 0 16px;position:relative;}
/* AIR marker. Deliberately a tiny caps word, not another coloured icon: the
   card already spends its one alarm colour on severe, and a second red thing
   would compete with it. This has to read as a category, not an escalation. */
.id-card-tag-air{font-size:8px;font-weight:800;letter-spacing:.08em;opacity:.8;border:1px solid currentColor;border-radius:3px;padding:0 3px;margin-left:4px;vertical-align:1px;}
/* The seating block. Sits BELOW the card, not on it — the card face is
   already dense and has been trimmed four times. This is for a host reading
   over someone's shoulder, so it can afford the room. */
.card-airborne{margin:0 0 18px;padding:12px 14px;border:1px solid var(--line);border-left:3px solid var(--pine);border-radius:12px;background:var(--paper);}
.card-airborne-title{font-size:12px;font-weight:800;letter-spacing:.05em;text-transform:uppercase;opacity:.7;margin:0 0 6px;}
.card-airborne-ask{font-size:15px;font-weight:700;margin:0 0 6px;line-height:1.35;}
.card-airborne-note{font-size:12.5px;opacity:.72;margin:0 0 6px;line-height:1.45;}
.card-airborne-looks{font-size:13px;font-weight:700;margin:0 0 6px;line-height:1.4;}
/* Quiet, not loud. This is corroboration, not another alarm — it earns its
   weight by being a plain statement of fact next to the ask. */
.card-airborne-confirmed{font-size:12.5px;font-weight:700;color:var(--pine);margin:8px 0 0;display:flex;align-items:center;gap:5px;}
.airborne-confirm-row{display:flex;align-items:flex-start;gap:8px;margin:10px 0 0;font-size:13px;line-height:1.4;cursor:pointer;}
.airborne-confirm-row input{margin-top:2px;flex:0 0 auto;}
.airborne-confirm-row em{display:block;font-style:normal;font-size:11.5px;opacity:.65;margin-top:2px;}
.pre-arrival-preview{font-size:12.5px;line-height:1.5;background:var(--paper);border:1px solid var(--line);border-radius:10px;padding:10px 12px;margin:0 0 10px;white-space:pre-wrap;}
.pre-arrival-actions{display:flex;gap:8px;flex-wrap:wrap;}
.lang-switch-row{display:flex;align-items:center;gap:8px;margin:0 0 16px;flex-wrap:wrap;}
.lang-switch-row label{font-size:11.5px;color:var(--ink-soft);}
.lang-switch-row select{width:auto;padding:6px 9px;border:1px solid var(--line);border-radius:7px;font-family:inherit;font-size:12.5px;background:var(--paper);color:var(--ink);}
.lang-switch-note{font-size:12.5px;color:var(--ink-soft);margin:0 0 14px;line-height:1.5;}
.id-card-footer{display:flex;justify-content:space-between;align-items:flex-start;gap:10px;font-size:11px;opacity:.72;letter-spacing:.05em;border-top:1px solid rgba(255,255,255,0.2);padding-top:10px;position:relative;margin-top:auto;}
.id-card-code{font-family:monospace;letter-spacing:.06em;word-break:break-all;font-variant-numeric:tabular-nums;text-shadow:-1px -1px 0 rgba(0,0,0,0.4),1px 1px 0 rgba(255,255,255,0.12);}
/* Hasib: "should the card say allergy id code above it" — a tiny caption
   above the code itself, matching the .id-card-brand-url pattern already
   used on the other side of this same footer (small, low-opacity fine
   print rather than a second heading), so it reads as a caption on the
   number, not a competing label. */
.id-card-code-block{display:flex;flex-direction:column;gap:2px;}
.id-card-code-label{font-size:8.5px;font-weight:600;letter-spacing:.1em;text-transform:uppercase;opacity:.78;}
/* Hasib: "make this parallel" — the brand side of the footer had no
   caption to match "Allergy ID code" on the code side, so the two columns
   read as mismatched heights/weights. Same tiny caption style, right above
   the pill, mirrors the code side exactly. */
.id-card-brand-label{font-size:8.5px;font-weight:600;letter-spacing:.1em;text-transform:uppercase;opacity:.78;margin-bottom:2px;}
.id-card-brand{display:flex;flex-direction:column;align-items:center;white-space:nowrap;font-weight:600;letter-spacing:.02em;text-align:center;}
/* Hasib: "this still looks too clunky, fix immediately" — the boxed
   compact pill (gradient fill + gold ring) here made the brand side of the
   footer the only "UI chrome" element on an otherwise plain, etched card
   face, so it stood out as a bolted-on badge rather than part of the card
   itself. Dropped the pill and gave the wordmark the exact same embossed
   text-shadow treatment .id-card-code already uses on the other side, so
   both footer columns now read as the same flat, engraved typography. */
.id-card-brand-word{font-family:Georgia,'Times New Roman',serif;font-weight:700;font-size:12.5px;letter-spacing:.02em;text-shadow:-1px -1px 0 rgba(0,0,0,0.4),1px 1px 0 rgba(255,255,255,0.12);}
/* Hasib: "should i have my site allergyspotter.com written somewhere" —
   small issuer-style print under the wordmark, the way a real membership
   card or passport prints the issuing body's site in the corner. Its own
   line, lighter weight/opacity, so it reads as fine print rather than
   competing with the bold "Allergy Spotter" wordmark above it. Switched
   .id-card-footer from align-items:baseline to flex-end for this — with a
   two-line right-hand item, baseline alignment was anchoring off the
   bottom line anyway, and flex-end reads cleaner for both the single-line
   card-id on the left and this new two-line block on the right. */
.id-card-brand-url{display:block;font-weight:400;opacity:.72;font-size:9.5px;letter-spacing:.03em;margin-top:2px;}
/* Hasib: "real ID cards have a front and back... a gentle 3D flip when
   someone taps the QR area." Wraps the existing .id-card (untouched,
   margin moved here instead) as the front face; the back is the same card
   surface showing the QR code, so tapping it feels like turning the actual
   card over rather than opening yet another overlay — now the ONLY way to
   reveal the QR, since the separate "Show full-screen to staff" flow this
   originally sat alongside has since been removed as redundant.
   Standard 3D-flip technique: the front face is normal-flow (so its height
   sets the container's height), the back face is absolutely positioned to
   match it exactly and pre-rotated 180deg so it lands right-side-up once
   the whole inner flips. */
/* Hasib, with a phone screenshot: "on the phone, the report a bug touches
   the card when you scroll, is that an issue we can fix" — it was: the
   fixed-position "Report a bug"/recall-alert tabs (see BUG_REPORT_WIDGET_HTML,
   both pinned to top:58% on opposite edges) are only ~34-40px wide, wider
   than section's 22px side padding, so on a narrow phone (where .app fills
   the full viewport instead of leaving its own desktop gutter) they
   overlapped straight onto the card's corner at that scroll position. Extra
   26px of margin on each side gives the card its own clearance from both
   tabs without touching the tabs themselves or any other page. */
.id-card-flip-outer{position:relative;perspective:1400px;margin:14px 26px 18px;cursor:pointer;border-radius:18px;transform:rotateX(var(--tilt-x,0deg)) rotateY(var(--tilt-y,0deg));transition:transform 0.5s var(--motion-entrance, cubic-bezier(.16,1,.3,1));}
.id-card-flip-outer:focus-visible{outline:2px solid var(--cream);outline-offset:3px;}
/* Hasib: "cursor/tilt-reactive sheen... catches light like a real card in
   your hand" — the tilt is applied HERE, on the outer wrapper, as its own
   small rotateX/rotateY driven by CSS custom properties that
   card-tilt.js-equivalent inline script below sets on mousemove/tilt. The
   INNER flip transform below is never touched, so the two 3D rotations
   just compose naturally: outer tilts as a flat object in its parent's
   space, then the flip's own depth projects inside that already-tilted
   local space — exactly like turning a real card over while it's tilted
   in your hand. While actively tracking the pointer/tilt the transition is
   shortened (see .tilt-tracking below) so it feels responsive; on
   mouseleave/settle it reverts to this slower one so it eases back flat. */
.id-card-flip-outer.tilt-tracking{transition:transform 0.1s linear;}@media (prefers-reduced-motion: reduce){html .id-card-flip-outer.tilt-tracking{transition:none;}}
@media (prefers-reduced-motion: reduce){.id-card-flip-outer{transition:none;}}
/* Hasib: "just remove the animation, it is better no" — the unboxing-style
   first-reveal animation (plain pop, then the envelope/seal/flap redesign,
   then the auto-scroll pacing attempts) is gone entirely. The card now just
   renders in its normal resting state on a brand-new view, same as any
   later visit — no wrapper, no keyframes, no scroll-on-load script. */
.id-card-flip-inner{position:relative;transition:transform 0.7s var(--motion-entrance, cubic-bezier(.16,1,.3,1));transform-style:preserve-3d;}
.id-card-flip-inner.flipped{transform:rotateY(180deg);}
@media (prefers-reduced-motion: reduce){.id-card-flip-inner{transition:none;}}
/* Hasib: "floating ambient shadow — a soft blurred shadow beneath the card
   that breathes slightly on load and shifts with the tilt above,
   reinforcing it as a physical object resting above the page." Sits
   outside the 3D flip transform entirely (sibling of .id-card-flip-inner,
   not a child of it) so it never gets caught up in the flip's own
   rotation or backface-visibility — it represents light/shadow in the
   PAGE's flat space, not the card's own 3D space. */
.id-card-ambient-shadow{position:absolute;left:10%;right:10%;bottom:-14px;height:26px;border-radius:50%;background:radial-gradient(ellipse at center,rgba(0,0,0,0.4) 0%,rgba(0,0,0,0) 72%);filter:blur(6px);z-index:-1;pointer-events:none;transform:translateX(var(--shadow-x,0px));animation:idCardShadowBreathe 4.5s ease-in-out infinite;}
@keyframes idCardShadowBreathe{0%,100%{opacity:.55;transform:translateX(var(--shadow-x,0px)) scale(0.96);}50%{opacity:.78;transform:translateX(var(--shadow-x,0px)) scale(1.03);}}
@media (prefers-reduced-motion: reduce){.id-card-ambient-shadow{animation:none;opacity:.65;}}
/* Same tilt build, the glare half — a moving highlight that tracks the
   same pointer/device-tilt position as the shadow above, so the two read
   as one consistent light source. Sits OUTSIDE the flip transform too
   (sibling of .id-card-flip-inner) since it represents light hitting
   whichever face currently happens to be visible, not something that
   should itself spin with the flip. */
.id-card-tilt-glare{position:absolute;inset:0;border-radius:18px;pointer-events:none;opacity:0;mix-blend-mode:overlay;background:radial-gradient(circle at var(--glare-x,50%) var(--glare-y,50%),rgba(255,255,255,0.6) 0%,rgba(255,255,255,0) 55%);transition:opacity 0.3s var(--motion-standard, cubic-bezier(.2,.75,.25,1));}
.id-card-flip-outer.tilt-tracking .id-card-tilt-glare{opacity:1;}
@media (prefers-reduced-motion: reduce){.id-card-tilt-glare{display:none;}}
/* Hasib: after seeing both in place, "remove these completely" — the
   holographic security strip (tried first as a rainbow, then as a
   toned-down severity-tinted pearlescent version) and the milled-edge
   border notch texture. Both fully removed: no CSS, no markup, no JS
   references to either left behind. */
.id-card-flip-face{backface-visibility:hidden;-webkit-backface-visibility:hidden;}
.id-card-flip-face.id-card-flip-back{position:absolute;inset:0;transform:rotateY(180deg);}
.id-card-flip-back .id-card{height:100%;display:flex;flex-direction:column;justify-content:center;}
/* Hasib: "guilloché security pattern — a very faint interwoven line
   pattern (the kind you see on banknotes and passport pages) behind the
   seal or across the card back — reinforces 'hard to fake, genuine
   document'." Approximated with concentric rings centered at each corner
   of an 80px tile — since adjacent tiles share corners, the rings
   continue seamlessly across tile edges into a continuous field of
   overlapping arcs when repeated, without needing a hand-drawn spirograph
   path.
   Hasib then flagged: "the circles blend in with the text, makes it
   harder to read... make it more premium." Two changes in response: (1)
   the pattern itself is fainter and finer now (opacity 0.5→0.22,
   stroke-width 0.6→0.4, one extra ring per corner for a denser, more
   delicate engraved feel — real guilloché work is a hairline, not
   something bold enough to compete with text), and (2) a soft radial
   vignette layer sits ON TOP of the pattern, dimming it specifically in
   the card's centre — exactly where the QR/label/badge live — while
   leaving it fully visible toward the corners. That's the same trick real
   security paper uses: the fine engraving stays crisp at the edges but
   the middle is kept clean for whatever's actually printed there. Belt
   and suspenders: the label/badge below also got their own solid-ish
   pill backgrounds, so legibility never depends on the vignette alone.
   Four background-image layers on top of each other now (vignette,
   guilloché, grain-noise, base gradient) — this selector's higher
   specificity overrides background-image/repeat/size wholesale versus
   the base .id-card rule, so all four have to be restated together here,
   not just the new one. Scoped to the back face only: the front already
   has the allergen tags/name to stay legible against, and doesn't need
   the extra texture. */
.id-card-flip-back .id-card{
  background-image:radial-gradient(ellipse 68% 58% at 50% 50%,rgba(15,46,36,0.55) 0%,rgba(15,46,36,0.22) 55%,rgba(15,46,36,0) 82%),url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='80' height='80'%3E%3Cg fill='none' stroke='white' stroke-width='0.4' opacity='0.22'%3E%3Ccircle cx='0' cy='0' r='8'/%3E%3Ccircle cx='0' cy='0' r='16'/%3E%3Ccircle cx='0' cy='0' r='24'/%3E%3Ccircle cx='0' cy='0' r='32'/%3E%3Ccircle cx='0' cy='0' r='40'/%3E%3Ccircle cx='80' cy='80' r='8'/%3E%3Ccircle cx='80' cy='80' r='16'/%3E%3Ccircle cx='80' cy='80' r='24'/%3E%3Ccircle cx='80' cy='80' r='32'/%3E%3Ccircle cx='80' cy='80' r='40'/%3E%3Ccircle cx='80' cy='0' r='8'/%3E%3Ccircle cx='80' cy='0' r='16'/%3E%3Ccircle cx='80' cy='0' r='24'/%3E%3Ccircle cx='80' cy='0' r='32'/%3E%3Ccircle cx='80' cy='0' r='40'/%3E%3Ccircle cx='0' cy='80' r='8'/%3E%3Ccircle cx='0' cy='80' r='16'/%3E%3Ccircle cx='0' cy='80' r='24'/%3E%3Ccircle cx='0' cy='80' r='32'/%3E%3Ccircle cx='0' cy='80' r='40'/%3E%3C/g%3E%3C/svg%3E"),url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='140' height='140'%3E%3Cfilter id='g'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='2' stitchTiles='stitch'/%3E%3CfeColorMatrix type='saturate' values='0'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23g)' opacity='0.045'/%3E%3C/svg%3E"),radial-gradient(circle at 20% 12%,var(--pine-light) 0%,var(--pine) 42%,var(--pine-dark) 100%);
  background-repeat:no-repeat,repeat,repeat,no-repeat;
  background-size:auto,80px 80px,140px 140px,auto;
}
/* Hasib: "if the qr code is on the back and you can flip it, do we need
   both qr codes... should we make the one on the card bigger" — the
   standalone "Show to staff" section below (its own QR + scan-frame) was
   dropped entirely (see the removed .qr-wrap markup), and this one, single
   remaining QR takes on its premium treatment instead: the same
   .qr-scan-wrap/.scan-frame shine-sweep + corner-bracket component used
   there, plus the same 8px "quiet zone" border a scanner actually needs
   around the code, both reused rather than reinvented. Bumped from 148px
   to 180px now it's the only QR on the card. Corner/hairline colours are
   overridden lighter than the default (pine-tinted, meant for the light
   paper section) since this sits on the card's own dark surface. Kept
   modest rather than matching the old standalone's up-to-220px size: this box has a fixed
   height (the back face is absolutely positioned to match the front's
   natural height, not independently sized) — that predicted risk landed:
   "fix the formatting and spacing immediately for laptops and phones, it
   is not good, shocking, fix it" after a later change (see the min-height
   note on .id-card above) shrank the front's natural height on a short
   allergen list below what this fixed QR block needed, clipping/cramming
   the back face. Response was two-sided: .id-card now guarantees enough
   room via min-height, AND this block itself was trimmed a little
   (180px->160px QR, tighter margins below) so it asks for less room in
   the first place — belt and suspenders, same as the guilloché
   legibility fix above. 160px is still comfortably larger than the old
   148px pre-consolidation size and reads clearly at arm's length. */
.id-card-back-qr-wrap{display:flex;justify-content:center;position:relative;}
.id-card-back-qr-wrap .qr-scan-wrap{box-shadow:inset 0 0 0 1px rgba(255,255,255,0.22),0 6px 16px rgba(0,0,0,0.32);}
.id-card-back-qr-wrap .qr-scan-wrap .scan-frame .frame-corner{border-color:rgba(255,255,255,0.85);}
.id-card-back-qr-wrap img{width:160px;height:160px;border:8px solid var(--paper);border-radius:12px;box-shadow:0 3px 12px rgba(0,0,0,0.15),inset 0 -3px 5px -3px rgba(0,0,0,0.2);display:block;}
/* Hasib: "the circles blend in with the text... keep the text pronounced
   and noticeable." Was plain text with no backdrop, sitting directly on
   the guilloché pattern — now a solid-ish pill, same family as
   .id-card-flip-badge below, so legibility never depends on how faint
   the pattern behind it happens to be. Top margin trimmed 12px->10px as
   part of the "shocking" spacing fix above. */
.id-card-back-label{display:flex;justify-content:center;margin:10px 0 0;position:relative;}
.id-card-back-label-chip{background:rgba(15,46,36,0.55);border:1px solid rgba(255,255,255,0.22);border-radius:999px;padding:5px 14px;font-size:11.5px;font-weight:700;letter-spacing:.06em;text-transform:uppercase;box-shadow:0 2px 8px rgba(0,0,0,0.22);}
.id-card-flip-badge-row{display:flex;justify-content:center;margin-top:14px;position:relative;}
.id-card-flip-badge{background:rgba(15,46,36,0.5);border:1px solid rgba(255,255,255,0.3);border-radius:999px;padding:5px 12px;font-size:10.5px;font-weight:700;letter-spacing:.02em;box-shadow:0 2px 8px rgba(0,0,0,0.22);}
.last-reviewed-line{text-align:center;font-size:12.5px;color:var(--ink-soft);margin:0 0 10px;}
/* Hasib: "fix the spacing so it looks slick and has space between each
   bit" — raised after the full-screen QR button was removed, since its
   old 10px margin-bottom (matched to sitting right under a short button)
   left barely any air once it was the note directly under the much
   larger flip card instead. Brought this whole run of stacked
   post-card elements (note/summary/nudges) up to a consistent ~16-18px
   rhythm, closer to the 18-22px already used for the bigger blocks above
   (Emergency button, flip card), rather than the mix of 10/14/16px it
   had before. */
.card-portable-note{text-align:center;font-size:12.5px;font-style:italic;color:var(--ink-soft);margin:0 0 8px;line-height:1.5;}
/* Hasib (screenshot): "is the shape of the allergy id good, or can it be
   more slick, premium, elegant, needs to look gorgeous. also text here is
   too clunky." The old flat pastel-fill chips wrapped awkwardly onto their
   own separate line and read as UI-kit-default rather than considered. This
   redesign does two things: (1) the descriptive sentence now sits on its
   own line, above a dedicated pills row, so pills never fight prose for
   horizontal space and wrap unpredictably; (2) both pills switch from solid
   pastel fills to a quiet outline-plus-dot treatment — a small solid dot
   carries the status colour, the border is a hairline in that same colour,
   the fill stays transparent. Reads as restrained jewellery, not a bright
   sticker, and both pills now share one identical shape/size so a row of
   them always sits level. */
.status-pills-row{display:flex;align-items:center;justify-content:center;flex-wrap:wrap;gap:8px;margin:0 0 16px;}
.freshness-pill{display:inline-flex;align-items:center;gap:6px;padding:3px 11px;border-radius:999px;font-size:10px;font-weight:700;letter-spacing:.05em;text-transform:uppercase;background:transparent;}
.freshness-pill::before{content:'';width:5px;height:5px;border-radius:50%;background:currentColor;flex-shrink:0;}
.freshness-pill.freshness-fresh{color:var(--safe);border:1px solid var(--safe);}
.freshness-pill.freshness-aging{color:var(--trace);border:1px solid var(--trace);}
.freshness-pill.freshness-stale{color:var(--caution);border:1px solid var(--caution);}
.freshness-pill.freshness-unknown{color:var(--unknown);border:1px solid var(--unknown);}
/* Hasib: "'Works offline' badge" — shows once this card's own service
   worker (see buildCardServiceWorkerScript) has actually cached this page
   for offline use, via the controllerchange listener + updateOfflineBadge()
   in the page's own script below. Reinstated (Aug 2026) after a session of
   chasing a flashing/reload-loop bug wrongly blamed on this feature — the
   real cause turned out to be a stale localStorage-cached edit token
   re-triggering computeOwnerKeyRecoveryUrl's redirect forever after a
   token regenerate, nothing to do with the service worker or this badge at
   all (see that function's own comment). Restored to its original design. */
.offline-ready-badge{display:inline-flex;align-items:center;gap:5px;padding:3px 11px;border-radius:999px;font-size:10px;font-weight:700;letter-spacing:.05em;text-transform:uppercase;background:transparent;color:var(--pine-dark);border:1px solid var(--pine-dark);opacity:0;transition:opacity .25s var(--motion-entrance, cubic-bezier(.16,1,.3,1));}
.offline-ready-badge.visible{opacity:1;}
.offline-ready-badge::before{content:'';width:5px;height:5px;border-radius:50%;background:currentColor;flex-shrink:0;}
/* Hasib: "the cardholder staring at their own card while waiting sees
   nothing [when staff scan it] ... show a live 'a staff member is
   reviewing your allergies now' state right on the card" — a passive
   reassurance pill, not a decision to make (that's still the separate
   Allow/Not now prompt below). Same display:none-by-default /
   inline-flex-when-shown toggle as the other status pills, but no opacity
   fade: unlike "Available offline" this one is rare and momentary, so it
   should never sit reserving invisible space in the row between scans. */
.reviewing-now-pill{display:none;align-items:center;gap:6px;padding:3px 11px;border-radius:999px;font-size:10px;font-weight:700;letter-spacing:.05em;text-transform:uppercase;background:transparent;color:var(--unknown);border:1px solid var(--unknown);}
.reviewing-now-pill::before{content:'';width:5px;height:5px;border-radius:50%;background:currentColor;flex-shrink:0;}
.usage-summary-line{text-align:center;font-size:11.5px;font-style:italic;color:var(--ink-soft);margin:0 0 16px;line-height:1.5;letter-spacing:.01em;}
.stale-nudge{background:var(--trace-bg);color:var(--trace);border:1.5px solid var(--trace);border-radius:10px;padding:10px 12px;font-size:12.5px;line-height:1.5;margin:0 0 18px;}
.epipen-expiry-nudge{background:var(--trace-bg);color:var(--trace);border:1.5px solid var(--trace);border-radius:10px;padding:10px 12px;font-size:12.5px;font-weight:600;line-height:1.5;margin:0 0 18px;}
.epipen-expiry-nudge.expired{background:var(--caution-bg);color:var(--caution);border-color:var(--caution);}
.card-nfc-btn{display:block;width:100%;margin:-6px 0 14px;padding:9px 14px;background:var(--paper);border:1.5px solid var(--line);border-radius:10px;font-size:12.5px;font-weight:600;color:var(--pine-dark);cursor:pointer;text-align:center;}
.card-nfc-btn:hover{border-color:var(--pine);}
.card-nfc-status{text-align:center;font-size:12.5px;margin:-10px 0 14px;line-height:1.5;}
.card-action-row{display:flex;flex-wrap:wrap;gap:8px;margin-top:10px;}
.card-action-row button,.card-action-row a{flex:1 1 45%;min-width:130px;margin-top:0;}
.id-identity-toggle{margin:8px 0 0;}
.id-identity-toggle summary{font-size:12.5px;color:var(--ink-soft);cursor:pointer;list-style:none;font-weight:600;}
.id-identity-toggle summary::-webkit-details-marker{display:none;}
.id-identity-toggle summary::before{content:'▸ ';}
.id-identity-toggle[open] summary::before{content:'▾ ';}
.id-identity-toggle input{margin-top:8px;}
.transition-checklist-toggle{margin:10px 0 0;border-top:1px dashed var(--line);padding-top:10px;}
.transition-checklist-toggle summary{font-size:12.5px;color:var(--ink-soft);cursor:pointer;list-style:none;font-weight:600;}
.transition-checklist-toggle summary::-webkit-details-marker{display:none;}
.transition-checklist-toggle summary::before{content:'▸ ';}
.transition-checklist-toggle[open] summary::before{content:'▾ ';}
.transition-checklist-list{margin:0;padding-left:18px;font-size:12.5px;color:var(--ink);line-height:1.6;}
.transition-checklist-list li{margin:0 0 4px;}
.voice-picker-toggle{margin:8px 0 0;}
.voice-picker-toggle summary{font-size:11.5px;color:var(--ink-soft);cursor:pointer;list-style:none;}
.voice-picker-toggle summary::-webkit-details-marker{display:none;}
.voice-picker-toggle summary::before{content:'▸ ';}
.voice-picker-toggle[open] summary::before{content:'▾ ';}
.voice-picker-toggle .voice-picker-row{display:flex;gap:8px;margin-top:8px;align-items:stretch;}
.voice-picker-toggle select{flex:1;min-width:0;padding:8px 10px;border:1.5px solid var(--line);border-radius:8px;font-family:inherit;font-size:12.5px;background:var(--paper);color:var(--ink);box-sizing:border-box;}
.voice-preview-btn{flex:none;padding:8px 12px;border:1.5px solid var(--line);border-radius:8px;font-family:inherit;font-size:12.5px;background:var(--paper);color:var(--ink);cursor:pointer;white-space:nowrap;}
.voice-preview-btn:hover{background:var(--line);}
.google-wallet-row{text-align:center;margin-top:14px;}
.google-wallet-microcopy{text-align:center;font-size:12.5px;color:var(--ink-soft);margin:0 0 8px;opacity:0;animation:walletMicrocopyIn 0.5s var(--motion-entrance, cubic-bezier(.16,1,.3,1)) 0.2s forwards;}@media (prefers-reduced-motion: reduce){html .google-wallet-microcopy{animation:none;opacity:1;transform:none;}}
/* Hasib: "google wallet and apple size wallet are not the same, can we
   amend the google wallet size so it matches the size of the apple wallet."
   Root cause: this used to be Google's own official
   add-to-wallet-button-primary.png badge in a plain <img>, sized with
   height:48px. That PNG bakes its own internal transparent padding
   around the visible black pill (a Google Wallet brand-asset convention),
   so setting the *image's* box to 48px tall left the actual black pill
   drawn inside it visibly shorter/narrower than Apple's button, which is a
   custom-built element with no baked-in padding to fight. Fixed by
   building Google's button the exact same way as Apple's own
   (.apple-wallet-btn below) — same box-sizing, height, padding, and
   border-radius — so the two are now pixel-identical by construction
   rather than two different asset formats trying to visually agree. See
   .google-wallet-btn for the actual button; the icon is a generic
   card-stack glyph (not Google's own multicolor "G", which their brand
   guidelines don't allow recoloring/embedding into a custom button) to
   stay symmetrical with Apple's own generic glyph.
   The gloss/shine effects below still work exactly as before — they were
   never tied to the image's own aspect ratio, just the wrapper's box, so
   the one-shot shine sweep on load still layers on top cleanly. */
/* Hasib: "fix the alignment, i want these parallel below each other" —
   both buttons were inline-flex with padding but no fixed width, so
   they sized themselves to their own text ("Add to Google Wallet" vs
   "Add to Apple Wallet" — one character different, a handful of pixels
   different once rendered). Each row still centers its button via
   text-align:center on the row, but two *different-width* centered boxes
   don't share a left edge, which is what threw the icons out of line when
   stacked. Giving both buttons the same explicit WALLET_BTN_WIDTH fixes
   it — same width + same centered row = identical left edge, guaranteed,
   not just "usually close enough". */
.google-wallet-btn{box-sizing:border-box;display:inline-flex;align-items:center;justify-content:center;gap:8px;width:288px;white-space:nowrap;background:#000;color:#fff;height:48px;min-height:48px;max-height:48px;padding:0 22px;border-radius:24px;text-decoration:none;font-weight:600;font-size:15px;font-family:'Google Sans',Roboto,-apple-system,BlinkMacSystemFont,Helvetica,Arial,sans-serif;line-height:1;-webkit-text-size-adjust:100%;text-size-adjust:100%;overflow:hidden;position:relative;z-index:1;}
/* Hasib: "this has something blue around it, i just want it black and
   white as it is, remove." That blue was a deliberate Google-blue glow
   added earlier to visually tell the two wallet buttons apart at a glance
   — since removed per this request. The distinct outline "wallet" glyph
   (below) still does that differentiation job on its own, so nothing is
   lost: same neutral black-shadow treatment as Apple's button now, no
   colour tint on either. */
/* Same fixed-left-offset treatment as .apple-wallet-glyph above, and for
   the same reason — pins this icon to the identical x-position across
   both buttons regardless of label length. */
.google-wallet-glyph{position:absolute;left:22px;top:50%;transform:translateY(-50%);width:22px;height:22px;flex-shrink:0;display:inline-block;background:url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><rect x="2" y="5" width="20" height="14" rx="3" fill="none" stroke="white" stroke-width="1.8"/><circle cx="17" cy="12" r="1.6" fill="white"/></svg>') no-repeat center/contain;}
.google-wallet-pulse-wrap{position:relative;display:inline-block;border-radius:24px;overflow:hidden;box-shadow:0 2px 8px rgba(0,0,0,0.25);transition:transform .45s var(--motion-spring, cubic-bezier(.34,1.56,.64,1));}
.google-wallet-pulse-wrap:hover{transform:translateY(-2px);}
.google-wallet-pulse-wrap::before{content:'';position:absolute;top:0;left:1px;right:1px;height:52%;
  background:linear-gradient(to bottom,rgba(255,255,255,0.32),rgba(255,255,255,0));
  border-radius:24px 24px 50% 50% / 24px 24px 100% 100%;pointer-events:none;}
.google-wallet-pulse-wrap::after{content:'';position:absolute;top:0;left:-60%;width:35%;height:100%;
  background:linear-gradient(115deg,transparent,rgba(255,255,255,0.75),transparent);
  transform:skewX(-20deg);animation:walletShine 1s var(--motion-entrance, cubic-bezier(.16,1,.3,1)) 0.6s 1;pointer-events:none;}
@keyframes walletShine{from{left:-60%;}to{left:130%;}}
@media (prefers-reduced-motion: reduce){.google-wallet-pulse-wrap::after{display:none;}}
@keyframes walletMicrocopyIn{from{opacity:0;transform:translateY(4px);}to{opacity:1;transform:translateY(0);}}
.apple-wallet-row{text-align:center;margin-top:14px;}
.wallet-view-only-note{text-align:center;font-size:12.5px;color:var(--ink-soft);margin:10px 0 0;line-height:1.4;}
.wallet-update-hint{display:flex;flex-direction:column;align-items:center;gap:3px;margin:8px 0 0;font-size:12.5px;color:var(--ink-soft);line-height:1.4;}
.wallet-update-hint span{font-weight:600;}
.scan-methods-note{display:flex;flex-direction:column;gap:6px;margin:0 0 10px;font-size:12.5px;color:var(--ink-soft);line-height:1.4;}
.scan-methods-note .scan-method-row{display:flex;align-items:center;gap:8px;}
.scan-methods-note .scan-method-icon{font-size:14px;line-height:1;flex-shrink:0;width:18px;text-align:center;}
.scan-methods-note .scan-methods-outro{display:flex;align-items:flex-start;gap:8px;margin-top:4px;padding-top:8px;border-top:1px solid var(--line);}
.scan-methods-note .scan-methods-outro .scan-method-icon{margin-top:1px;}
.wallet-help-details{margin:10px 0 0;padding:12px 16px;}
.wallet-help-details summary{font-size:12.5px;font-weight:600;cursor:pointer;}
.wallet-help-details .wallet-update-hint{margin-top:10px;}
.apple-wallet-btn{position:relative;box-sizing:border-box;display:inline-flex;align-items:center;justify-content:center;gap:8px;width:288px;white-space:nowrap;background:#000;color:#fff;height:48px;min-height:48px;max-height:48px;padding:0 22px;border-radius:24px;text-decoration:none;font-weight:600;font-size:15px;font-family:-apple-system,BlinkMacSystemFont,"SF Pro Text",Helvetica,Arial,sans-serif;line-height:1;-webkit-text-size-adjust:100%;text-size-adjust:100%;overflow:hidden;box-shadow:0 2px 8px rgba(0,0,0,0.25);transition:transform .45s var(--motion-spring, cubic-bezier(.34,1.56,.64,1));}@media (prefers-reduced-motion: reduce){html .apple-wallet-btn{transition:none;}}
.apple-wallet-btn:hover{transform:translateY(-2px);}
/* Hasib: "just move the logo so they are perpendicular below each other,
   don't move the buttons" — the buttons themselves were already the same
   width/position (see WALLET_BTN width fix above), but the icon was still
   laid out as part of the centered icon+text flex group, so its x-position
   still shifted depending on how long each button's own label rendered
   ("Add to Google Wallet" vs "Add to Apple Wallet" aren't the same pixel
   width). Taking the glyph out of flex flow and pinning it at a fixed
   left offset — identical in both buttons, since both share the same
   width/padding — means the icons now land at the exact same x whichever
   button they're in, independent of the label text next to them. The
   button box itself is completely untouched. */
.apple-wallet-glyph{position:absolute;left:22px;top:50%;transform:translateY(-50%);width:22px;height:22px;flex-shrink:0;display:inline-block;background:url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path fill="white" d="M4 6a2 2 0 0 1 2-2h12a2 2 0 0 1 2 2v3H4zM4 11h16v7a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2z"/></svg>') no-repeat center/contain;}
#shareCardMsg{text-align:center;font-size:11.5px;color:var(--ink-soft);margin:6px 0 0;min-height:14px;}
#shareCardMsg:empty{margin:0;min-height:0;}
.card-toast{position:fixed;left:50%;bottom:22px;transform:translate(-50%,16px);background:#16261f;color:#fff;padding:11px 20px;border-radius:999px;font-size:13px;font-weight:600;box-shadow:0 6px 20px rgba(0,0,0,0.32);opacity:0;transition:opacity 0.25s var(--motion-entrance, cubic-bezier(.16,1,.3,1)),transform 0.25s var(--motion-entrance, cubic-bezier(.16,1,.3,1));pointer-events:none;z-index:200;max-width:88%;text-align:center;}
.card-toast.show{opacity:1;transform:translate(-50%,0);}
@media (prefers-reduced-motion: reduce){.card-toast{transition:none;}}
/* Hasib: "the close button on the signs of an allergic reaction is cut off
   on the phone when opened" — this overlay centered its content with no
   scrolling at all, so once the "Signs of a severe reaction" details were
   expanded (or just on a short phone screen with several optional sections
   showing — EpiPen note, contact, identity, etc.) the total height could
   exceed the viewport with no way to reach anything below the fold,
   including the Close button itself. overflow-y:auto lets it scroll when
   content is taller than the screen while still centering it when it
   isn't; -webkit-overflow-scrolling keeps that scroll smooth on iOS. */
.card-modal-overlay{position:fixed;inset:0;background:rgba(15,28,23,0.82);display:none;align-items:center;justify-content:center;z-index:100;padding:20px;overflow-y:auto;-webkit-overflow-scrolling:touch;}
.card-modal-overlay.open{display:flex;}
/* Task: one-tap emergency mode — the trigger sits at the very top of the
   card (first interactive thing on the page, before even the manage/QR
   sections) since a real reaction is exactly the moment nobody has time to
   scroll and hunt. The overlay itself is deliberately the loudest, highest-
   contrast surface on the whole page (solid --caution red, not the paper/
   cream used everywhere else) so a stranger unfamiliar with this app can
   still immediately tell it's the "act now" screen.
   Layout pass (user feedback: "make the layout better"):
   1. (Later removed per further feedback: a pulsing ring around the trigger
      button read as "flashing" rather than premium, so it's back to a still
      gradient + bevel treatment instead — same call made on the QR frame
      and the Google Wallet button below, both of which dropped their own
      looping pulse rings for the same reason.)
   2. The epipen reminder now renders ABOVE "Call 999" instead of below it —
      the reminder's own copy says "use it now, THEN call 999" (standard
      anaphylaxis first aid: adrenaline first), so the visual order used to
      contradict its own text. Fixed so what a guest reads top-to-bottom
      matches what they should actually do.
   3. The epipen reminder is no longer styled identically to the tappable
      "Call [contact]" button below it (same solid border/fill made an
      inert note look exactly like a second button). Now a dashed border on
      a lighter fill, so it reads as "read this" rather than "tap this". */
/* Same "extruded metal edge" language as the id-card/header bevel work
   above: brighter top highlight + a faint bottom shadow line, plus a
   deeper ambient glow so the button reads as the single most substantial,
   premium-feeling object on the page — appropriate, since it's also the
   most important one. */
.emergency-trigger-btn{position:relative;width:100%;padding:14px 18px;margin:0 0 22px;border:1px solid rgba(255,255,255,0.22);border-radius:14px;
  background:radial-gradient(circle at 22% 18%,#dd6a41 0%,var(--caution) 55%,#8f3a1f 100%);
  color:#fff;font-size:15px;font-weight:700;cursor:pointer;font-family:inherit;letter-spacing:.05em;text-transform:uppercase;
  display:flex;align-items:center;justify-content:center;gap:11px;
  box-shadow:0 12px 26px rgba(193,80,46,0.34),0 2px 6px rgba(0,0,0,0.22),inset 0 1px 0 rgba(255,255,255,0.3),inset 0 -1px 0 rgba(0,0,0,0.18);
  transition:transform .45s var(--motion-spring, cubic-bezier(.34,1.56,.64,1));}
.emergency-trigger-btn:active{transform:scale(0.99);}
/* The BUTTON's own spring, which is decoration and goes — unlike the hold ring
   above it, whose width is the countdown. The press and hover transforms still
   apply, instantly; only the animated travel is dropped, and nothing here is
   read by initEmergencyQuickDial. */
@media (prefers-reduced-motion: reduce){html .emergency-trigger-btn{transition:none;}}
.emergency-trigger-btn:hover{transform:translateY(-1px);}
/* Hasib: "quick-dial on the emergency contact — a direct tel: shortcut
   that skips the full emergency overlay tap sequence when someone's
   actually in a hurry." A visible, always-on tel: link was tried and
   removed earlier in this project precisely because it cluttered this
   spot once the overlay already covered the same info (see the
   "quickline removed" work). So this doesn't reintroduce a second visible
   button — instead the SAME Emergency button gains a press-and-hold
   gesture: a normal tap still opens the full guided overlay exactly as
   before, but holding it down dials the emergency contact (or 999 if none
   is set) directly, no overlay in between. The fill bar along the bottom
   edge is the only visual trace of this — it only appears while actively
   held, syncs to the same 550ms threshold as the JS timer, and snaps back
   instantly (no lingering animation) if released early. */
.emergency-trigger-holdring{position:absolute;left:0;bottom:0;height:3px;width:0%;background:rgba(255,255,255,0.85);border-radius:0 0 14px 14px;pointer-events:none;transition:width 0s linear;}
/* NOT COVERED FOR REDUCED MOTION, AND MUST NOT BE. This 550ms is not
   decoration — it is the countdown for the press-and-hold that dials
   data-quickdial, and it is matched exactly to HOLD_MS in
   initEmergencyQuickDial (src/index.js). A reduced-motion rule setting
   transition:none here was added in 61e9a4b and removed again straight away:
   with no transition the ring paints full at 0ms while the timer still has
   550ms to run, so someone with the preference on sees "done", releases,
   pointerup cancels the hold, and NO CALL IS PLACED. On the emergency path,
   for the people most likely to have that preference set.
   A 3px bar filling on a hold the user is deliberately performing is not what
   prefers-reduced-motion is about, and the width change here carries the
   information rather than dressing it. scripts/motion-audit.mjs knows this
   shape now — see PROGRESS_SELECTOR — so the sweep will not pick it up again. */
/* Layout-animating on purpose, and audited as such: see the note above. The
   width IS the countdown, read by initEmergencyQuickDial's HOLD_MS, so this is
   not a paint-performance target — and border-radius:0 0 14px 14px would square
   off under a scale anyway. */
.emergency-trigger-btn.holding .emergency-trigger-holdring{width:100%;transition:width 550ms linear;}
/* Hasib: "do i need to put somewhere under the emergency button explaining
   [shake-to-emergency]" — yes, since a shake gesture has no visual affordance
   at all (unlike the hold-to-dial fill bar right above, which is at least
   visible once you're already pressing). One quiet line, same muted note
   treatment used elsewhere on this page, pulled up close under the button
   with a negative top margin rather than adding a second full paragraph's
   worth of vertical space. */
.emergency-shake-hint{text-align:center;font-size:12.5px;color:var(--ink-soft);margin:-14px 0 18px;line-height:1.5;}
.emergency-trigger-icon{width:26px;height:26px;flex-shrink:0;padding-top:1px;border-radius:50%;background:rgba(255,255,255,0.18);display:flex;align-items:center;justify-content:center;font-size:14px;box-shadow:inset 0 0 0 1px rgba(255,255,255,0.32);}
.emergency-trigger-label{text-shadow:0 1px 2px rgba(0,0,0,0.22);}
#emergencyOverlay{background:radial-gradient(circle at 30% -8%,#dd6a41 0%,var(--caution) 55%,#7c3119 100%);}
/* margin:auto IS THE FIX, AND IT IS NOT A COSMETIC MARGIN.
   Confirmed on a real iPhone: a populated emergency overlay opened with its
   content sliced at the top of the viewport, the heading and the line "Use your
   adrenaline auto-injector now, then call 999 if you haven't already" entirely
   off-screen. Scrolling down worked. Scrolling up did nothing.
   The parent .card-modal-overlay is a flex container with align-items:center and
   overflow-y:auto. Centring a child TALLER than the container overflows it
   equally at both edges, and a scroll container cannot reach content past its
   start edge — measured at 375x667, the heading sat at -78px with scrollTop
   already 0 and 98px of scroll available only downward.
   An auto margin on the cross axis absorbs POSITIVE free space and resolves to
   zero when the free space is negative. So this centres exactly as before while
   the overlay fits, and pins the content to the top — fully scrollable — the
   moment it does not. Both cases, one declaration.
   NOT align-items:flex-start on the parent: .card-modal-overlay is shared with
   the Home Screen tip and the menu scanner, and top-aligning those two to fix
   this one would be a visual change to two modals that never had the problem.
   NOT the max-height:86vh;overflow-y:auto shape .menu-scan-inner uses: that one
   is a panel with its own background, so a nested scroll region reads as a
   scrollable card; this overlay's inner is transparent on a full-bleed gradient,
   where a nested scrollbox reads as the content simply stopping. And 86vh is an
   iOS trap of its own — vh is the LARGE viewport there, so it can still exceed
   the visible area while the URL bar is showing, which is the very condition
   that produced this bug.
   ON A SHORT VIEWPORT (landscape, ~375px tall) this makes everything REACHABLE,
   not comfortable: the content is ~823px, so it starts at the top and scrolls
   through its full height. Nothing is lost; it is a long scroll.
   Guarded by test_emergency_overlay_reachable.mjs, which can only assert this
   declaration is present — jsdom does no layout. Read that file's label before
   trusting it for more than that. */
.emergency-overlay-inner{text-align:center;width:100%;max-width:360px;margin:auto;}
.emergency-overlay-title{color:#fff;font-size:26px;font-weight:800;margin:0 0 22px;letter-spacing:.03em;text-shadow:0 1px 3px rgba(0,0,0,0.25);}
.emergency-overlay-call{display:block;background:#fff;color:var(--caution);border-radius:14px;padding:20px;font-size:22px;font-weight:800;text-decoration:none;margin:0 0 14px;letter-spacing:.01em;box-shadow:0 8px 22px rgba(0,0,0,0.28),inset 0 -2px 0 rgba(0,0,0,0.05);}
.emergency-overlay-epipen{background:rgba(255,255,255,0.10);border:1.5px dashed rgba(255,255,255,0.55);color:#fff;border-radius:10px;padding:13px 14px;font-size:14px;font-weight:600;line-height:1.5;margin:0 0 14px;}
.emergency-overlay-epipen-expired{display:block;margin-top:8px;font-size:12px;font-weight:700;color:#ffd9c4;}
/* Beside the EpiPen line, not in the identity block. Deliberately styled to
   match it rather than the "Also available" secondary block: both are
   treatment-relevant medical fact for whoever is treating a reaction. */
.emergency-overlay-nonfood{background:rgba(255,255,255,0.10);border:1.5px dashed rgba(255,255,255,0.55);color:#fff;border-radius:10px;padding:13px 14px;font-size:14px;font-weight:600;line-height:1.5;margin:0 0 14px;}
.emergency-overlay-nonfood-note{display:block;margin-top:8px;font-size:12px;font-weight:600;color:#e6ded0;}
/* The cardholder's own notice that their school changed this card. Quiet by
   design — it is a record, not an alert — but permanent, not a toast. */
.id-card-synced{font-size:11.5px;color:var(--ink-soft);margin:8px 0 0;line-height:1.5;}
/* Hasib: "can it be better" on the emergency screen -> visually step down
   everything after the EpiPen reminder + Call 999 pair. Those two are the
   only truly time-critical actions and the only ones kept at full size/
   contrast; contacting someone else, the ambulance-crew details, and a
   parent's note are all genuinely useful but secondary — under real stress
   there should be zero hesitation about what to do first, so this wrapper
   shrinks type size, drops the button-like shadow, and adds one small
   label rather than leaving five same-weight blocks stacked in a row. */
.emergency-secondary{margin:2px 0 14px;}
.emergency-secondary-label{font-size:10.5px;font-weight:700;letter-spacing:.08em;text-transform:uppercase;color:rgba(255,255,255,0.55);margin:0 0 8px;}
.emergency-secondary .emergency-overlay-contact{display:block;background:rgba(255,255,255,0.10);border:1.5px solid rgba(255,255,255,0.45);color:#fff;border-radius:12px;padding:11px 14px;font-size:14px;font-weight:700;text-decoration:none;margin:0 0 8px;box-shadow:none;}
.emergency-secondary .emergency-overlay-identity{background:rgba(255,255,255,0.08);border:1.5px solid rgba(255,255,255,0.3);color:#fff;border-radius:10px;padding:10px 12px;font-size:12.5px;line-height:1.55;text-align:left;margin:0 0 8px;}
.emergency-secondary .emergency-overlay-identity-label{font-size:10px;font-weight:700;letter-spacing:.06em;text-transform:uppercase;opacity:.75;margin-bottom:3px;}
.emergency-secondary .emergency-overlay-parent-note{background:rgba(255,255,255,0.08);border:1.5px dashed rgba(255,255,255,0.3);color:#fff;border-radius:10px;padding:9px 12px;font-size:12.5px;line-height:1.5;text-align:left;font-style:italic;margin:0 0 8px;}
.emergency-secondary .emergency-overlay-identity:last-child,.emergency-secondary .emergency-overlay-contact:last-child,.emergency-secondary .emergency-overlay-parent-note:last-child{margin-bottom:0;}
.emergency-overlay-close{margin-top:10px;background:transparent;border:1.5px solid rgba(255,255,255,0.7);border-radius:999px;padding:10px 22px;font-size:13px;font-weight:700;color:#fff;cursor:pointer;font-family:inherit;}
.emergency-sos-btn{cursor:pointer;font-family:inherit;width:100%;}
.emergency-sos-countdown{display:flex;align-items:center;justify-content:space-between;gap:10px;background:rgba(255,255,255,0.14);border:1.5px solid rgba(255,255,255,0.75);color:#fff;border-radius:14px;padding:14px 16px;margin:0 0 14px;font-weight:700;font-size:15px;}
.emergency-sos-cancel{cursor:pointer;font-family:inherit;background:#fff;color:var(--caution);border:none;border-radius:10px;padding:8px 16px;font-size:13.5px;font-weight:800;flex-shrink:0;}
.emergency-sos-locstatus{color:#fff;font-size:12.5px;font-weight:600;text-align:center;margin:8px 0 0;opacity:0.9;}
.emergency-sos-locstatus.is-ready{opacity:0.75;}
.emergency-sos-locstatus.is-problem{opacity:1;background:rgba(0,0,0,0.22);border-radius:10px;padding:7px 10px;}
.emergency-sos-result{color:#fff;font-size:13.5px;font-weight:600;text-align:center;background:rgba(255,255,255,0.14);border:1.5px solid rgba(255,255,255,0.55);border-radius:12px;padding:10px 14px;margin:0 0 14px;}
.emergency-guidance{background:rgba(255,255,255,0.10);border:1.5px dashed rgba(255,255,255,0.55);border-radius:10px;padding:0 14px;margin:0 0 14px;text-align:left;color:#fff;}
.emergency-guidance summary{cursor:pointer;padding:11px 0;font-size:13.5px;font-weight:700;list-style:none;}
.emergency-guidance summary::-webkit-details-marker{display:none;}
.emergency-guidance summary::before{content:'▸ ';}
.emergency-guidance[open] summary::before{content:'▾ ';}
.emergency-guidance-body{padding:0 0 12px;font-size:12.5px;line-height:1.55;}
.emergency-guidance-body h4{margin:10px 0 4px;font-size:11px;text-transform:uppercase;letter-spacing:.04em;opacity:.85;}
.emergency-guidance-body ul,.emergency-guidance-body ol{margin:0;padding-left:18px;}
.emergency-guidance-body li{margin:0 0 4px;}
.emergency-guidance-body a{color:#fff;}
/* Deliberately styled as plain, muted text — not a button, not the caution
   colour, nothing that could be mistaken for a third "call now" action next
   to the loud 999/emergency-contact buttons above. This is Allergy UK's
   general advice line, not an emergency line (their own site says so
   explicitly), so it has to read as calm and secondary or it risks pulling
   attention away from 999 during an actual reaction. */
.emergency-overlay-helpline{font-size:12.5px;color:rgba(255,255,255,0.75);margin:0 0 14px;line-height:1.5;}
.emergency-overlay-helpline a{color:#fff;font-weight:700;text-decoration:underline;}
/* Elite scanning UI polish (Task #161): a shared "viewfinder" motif — four
   corner brackets, decorative and pointer-events:none so nothing underneath
   is ever blocked — used both places scanning happens on this page: around
   the guest's own QR (held up for STAFF to scan) and around the live camera
   view (guest's camera pointed at a MENU's own QR code). The camera version
   additionally gets a moving scan-line (see .menu-scan-video-wrap below)
   since it's actively hunting for a code; a static QR that's already fully
   visible doesn't need one. */
.scan-frame{position:absolute;inset:0;pointer-events:none;}
.scan-frame .frame-corner{position:absolute;width:22px;height:22px;border-style:solid;border-width:0;}
.scan-frame .frame-corner.tl{top:6px;left:6px;border-top-width:3px;border-left-width:3px;border-radius:6px 0 0 0;}
.scan-frame .frame-corner.tr{top:6px;right:6px;border-top-width:3px;border-right-width:3px;border-radius:0 6px 0 0;}
.scan-frame .frame-corner.bl{bottom:6px;left:6px;border-bottom-width:3px;border-left-width:3px;border-radius:0 0 0 6px;}
.scan-frame .frame-corner.br{bottom:6px;right:6px;border-bottom-width:3px;border-right-width:3px;border-radius:0 0 6px 0;}
/* Around the QR a guest holds up to staff — corner brackets (brand-tinted,
   since this sits on the light card background), plus a one-time shine
   sweep on load rather than a continuous pulsing ring. User feedback: the
   old looping pulse read as "flashing", not premium — same call already
   made on the emergency button and the id-card itself, both of which use a
   single shine sweep instead of a repeating effect.
   The soft 6px halo ring that used to sit around this was later flagged as
   reading like a flat grey border, so it's gone — replaced with a crisp
   1px hairline right at the edge and a tighter, more precise elevation
   shadow instead of a wide diffuse one. Note the QR image's own white
   border underneath (see .id-card-back-qr-wrap img, its only user now that
   the standalone "Show to staff" section is gone) stays untouched
   regardless — that's the quiet zone a scanner actually needs around the
   code to read it reliably, not decoration. */
.qr-scan-wrap{position:relative;display:inline-block;overflow:hidden;border-radius:12px;box-shadow:inset 0 0 0 1px rgba(13,74,55,0.14),0 6px 16px rgba(13,74,55,0.16);}
.qr-scan-wrap .scan-frame .frame-corner{border-color:rgba(13,74,55,0.55);}
.qr-scan-wrap::after{content:'';position:absolute;top:0;left:-60%;width:35%;height:100%;
  background:linear-gradient(115deg,transparent,rgba(255,255,255,0.65),transparent);
  transform:skewX(-20deg);pointer-events:none;}
/* Hasib: "one-shot glass sweep on flip — a single light streak crossing
   the QR frame the moment the card lands on its back face, not looping,
   fires once per flip like the haptic click." This exact shine element
   already existed but was wired to play unconditionally on page load
   (0.5s after render) — since the QR only lives on the FLIP-BACK face
   (backface-visibility:hidden until rotated past 90deg), that meant it was
   always finishing before anyone had a reason to look at it. Re-gated here
   to the .flipped class instead: 0.32s animation-delay lines it up with
   roughly the halfway point of the 0.7s flip transition (see
   .id-card-flip-inner above), which is when the back face actually swings
   into view — so the streak now reads as light catching the glass right as
   the card lands, not a page-load flourish nobody sees. Re-fires cleanly
   every flip since toggling the class off then on again is a fresh
   animation trigger each time. */
.id-card-flip-inner.flipped .qr-scan-wrap::after{animation:qrShine 0.55s var(--motion-entrance, cubic-bezier(.16,1,.3,1)) 0.32s 1 both;}@media (prefers-reduced-motion: reduce){html .id-card-flip-inner.flipped .qr-scan-wrap::after{animation:none;}}
@keyframes qrShine{from{left:-60%;}to{left:130%;}}
@media (prefers-reduced-motion: reduce){.qr-scan-wrap::after{display:none;}.scan-frame .frame-scanline{animation:none;top:48%;opacity:0.5;}}
/* margin:auto for the same reason as .emergency-overlay-inner, and it changes
   nothing visible today. Measured, this panel is 266-286px tall against every
   viewport tried, so it fits and an auto margin centres it exactly as
   align-items:center did. It is here because the invariant is not "does this
   overflow now" but "can it be reached if it ever does" — and this one's body
   is written at runtime by homeScreenInstructions(), which returns different
   text per platform and is the kind of thing that grows. The card's emergency
   overlay was fine too, until a card carried enough to fill it.
   test_emergency_overlay_reachable.mjs derives this requirement from the page
   rather than from a list, which is how it flagged this panel. */
.home-screen-tip-inner{background:var(--paper);border-radius:14px;padding:22px;max-width:340px;width:100%;margin:auto;box-shadow:0 12px 30px rgba(0,0,0,0.3);}
.home-screen-tip-title{font-size:15px;font-weight:700;margin:0 0 10px;}
.home-screen-tip-text{font-size:12.5px;color:var(--ink-soft);line-height:1.6;margin:0 0 16px;}
.home-screen-tip-inner button{width:100%;padding:11px;border:none;border-radius:8px;background:var(--pine);color:var(--cream);font-size:13.5px;font-weight:700;cursor:pointer;font-family:inherit;}
.result-item{display:flex;justify-content:space-between;align-items:center;padding:9px 12px;border-radius:8px;margin-bottom:6px;font-size:13.5px;gap:10px;}
.result-item.safe{background:var(--safe-bg);color:var(--safe);}
.result-item.avoid{background:var(--caution-bg);color:var(--caution);}
.result-item.trace{background:var(--trace-bg);color:var(--trace);}
.result-item.unknown{background:var(--unknown-bg);color:var(--unknown);}
/* Menu-QR scan modal — the whole point of this feature is it works at ANY
   restaurant's own menu QR code, on or off this platform, so it lives on the
   card view page itself rather than anywhere tied to a specific restaurant.
   Reuses the same .result-item safe/avoid/trace/unknown language as the
   dish-by-dish scoring language elsewhere in this app, so a guest never has
   to learn a second colour system. */
.menu-scan-inner{background:var(--paper);border-radius:14px;padding:22px;max-width:420px;width:100%;max-height:86vh;overflow-y:auto;box-shadow:0 12px 30px rgba(0,0,0,0.3);}
.menu-scan-title{font-size:15px;font-weight:700;margin:0 0 8px;}
.menu-scan-disclaimer{font-size:12.5px;color:var(--ink-soft);line-height:1.5;background:var(--trace-bg);color:var(--trace);border-radius:8px;padding:8px 10px;margin:0 0 14px;}
.menu-scan-results-disclaimer{font-size:12.5px;font-weight:600;color:var(--trace);line-height:1.5;background:var(--trace-bg);border-radius:8px;padding:8px 10px;margin:10px 0 0;}
.menu-scan-video-wrap{position:relative;border-radius:10px;overflow:hidden;background:#000;margin-bottom:10px;}
.menu-scan-video-wrap video{width:100%;display:block;max-height:260px;object-fit:cover;}
.menu-scan-video-wrap .scan-frame .frame-corner{border-color:rgba(120,255,180,0.9);}
.menu-scan-video-wrap .scan-frame .frame-scanline{position:absolute;left:10%;right:10%;height:2px;background:linear-gradient(90deg,transparent,rgba(120,255,180,0.95),transparent);animation:scanLineMove 2.1s ease-in-out infinite;}@media (prefers-reduced-motion: reduce){html .menu-scan-video-wrap .scan-frame .frame-scanline{animation:none;}}
@keyframes scanLineMove{0%{top:12%;opacity:0;}12%{opacity:1;}50%{top:85%;opacity:1;}62%{opacity:0;}100%{top:12%;opacity:0;}}
.menu-scan-url-row{display:flex;gap:8px;}
.menu-scan-url-row input{flex:1;}
/* Task #169: compact tab picker replacing the old stacked "— or —" list of
   four input methods — one method's controls visible at a time. */
.menu-scan-tabs{display:flex;gap:4px;background:var(--cream);border-radius:10px;padding:4px;margin:0 0 12px;}
.menu-scan-tab{flex:1;padding:8px 4px;border:none;border-radius:7px;background:transparent;color:var(--ink-soft);font-size:12px;font-weight:600;font-family:inherit;cursor:pointer;text-align:center;}
.menu-scan-tab.active{background:var(--paper);color:var(--ink);box-shadow:0 1px 4px rgba(0,0,0,0.12);}
.menu-scan-panel{margin:0 0 6px;}
.menu-scan-panel textarea{width:100%;margin-top:0;padding:9px 10px;border:1.5px solid var(--line);border-radius:8px;font-family:inherit;font-size:13px;resize:vertical;box-sizing:border-box;}
.menu-scan-panel-empty{font-size:12.5px;color:var(--ink-soft);text-align:center;padding:10px 0;}
/* Task #168: torch toggle overlaid on the live camera view, top-right so it
   never covers the viewfinder frame itself. */
.menu-scan-torch-btn{position:absolute;top:8px;right:8px;width:36px;height:36px;border-radius:50%;border:none;background:rgba(0,0,0,0.5);color:#fff;font-size:16px;cursor:pointer;display:flex;align-items:center;justify-content:center;z-index:1;}
.menu-scan-torch-btn.on{background:rgba(255,196,0,0.92);color:#1c332b;}
/* Task #167: preview + note shown once a photo is chosen, before/after
   client-side compression. */
.menu-scan-photo-preview-wrap{margin-top:10px;text-align:center;}
.menu-scan-photo-preview-wrap img{max-width:100%;max-height:180px;border-radius:8px;object-fit:contain;background:#000;}
.menu-scan-photo-note{font-size:12.5px;color:var(--ink-soft);margin:6px 0 0;}
.menu-scan-photo-thumbs{display:flex;flex-wrap:wrap;gap:8px;justify-content:center;}
.menu-scan-photo-thumb{position:relative;width:72px;height:72px;border-radius:8px;overflow:hidden;background:#000;flex:0 0 auto;}
.menu-scan-photo-thumb img{width:100%;height:100%;object-fit:cover;display:block;}
.menu-scan-photo-thumb-remove{position:absolute;top:2px;right:2px;width:20px;height:20px;border-radius:50%;border:none;background:rgba(0,0,0,0.65);color:#fff;font-size:11px;line-height:1;cursor:pointer;padding:0;}
.menu-scan-photo-actions{display:flex;gap:8px;justify-content:center;margin-top:8px;flex-wrap:wrap;}
.menu-scan-status{font-size:12.5px;color:var(--ink-soft);margin:8px 0;min-height:14px;}
.menu-scan-verified-banner{font-size:12.5px;font-weight:700;color:var(--safe);background:var(--safe-bg);border-radius:8px;padding:8px 11px;margin:0 0 8px;}
.menu-scan-verified-banner.flash{animation:menuScanVerifiedFlash 0.6s var(--motion-entrance, cubic-bezier(.16,1,.3,1));}
@keyframes menuScanVerifiedFlash{0%{transform:scale(0.92);opacity:0;box-shadow:0 0 0 0 rgba(61,122,78,0.5);}55%{transform:scale(1.03);opacity:1;box-shadow:0 0 0 6px rgba(61,122,78,0);}100%{transform:scale(1);opacity:1;box-shadow:0 0 0 0 rgba(61,122,78,0);}}
@media (prefers-reduced-motion: reduce){.menu-scan-verified-banner.flash{animation:none;}}
.menu-scan-results{margin-top:10px;}
/* Bug fix (Hasib): guaranteed "open the real menu" fallback shown when a
   scan of a link that's clearly one of our own restaurant menus still
   fails for some reason — see showMenuScanOpenDirectlyFallback. Neutral
   info styling (not the green "safe"/verified look above) since this isn't
   confirming anything about allergens, just offering a way forward. */
.menu-scan-open-fallback{font-size:12.5px;color:var(--ink);background:var(--card-bg,#f6f1e4);border:1px solid var(--border,#d8cfb8);border-radius:8px;padding:9px 11px;margin:8px 0 0;display:flex;align-items:center;gap:8px;flex-wrap:wrap;}
.menu-scan-open-fallback a{font-weight:700;color:var(--brand,#0d4a37);text-decoration:underline;}
.menu-scan-close{width:100%;margin-top:14px;padding:11px;border:none;border-radius:8px;background:var(--pine);color:var(--cream);font-size:13.5px;font-weight:700;cursor:pointer;font-family:inherit;}
.menu-scan-close.secondary-close{background:transparent;color:var(--ink-soft);border:1.5px solid var(--line);margin-top:8px;}
.menu-scan-guidance{font-size:12.5px;color:var(--ink-soft);margin:0 0 12px;}
.menu-scan-guidance summary{cursor:pointer;font-weight:700;color:var(--ink);}
.menu-scan-guidance p{line-height:1.6;margin:8px 0 0;}
.menu-scan-recent{margin-top:12px;}
/* Task #161: the list itself grows one row per distinct menu a guest scans
   (up to the server's own cap of 10 — see apiCardMenuScans), which starts
   to genuinely dominate the modal once it's not just 1-2 entries. Collapsed
   behind a <details>/<summary> by default (same declutter pattern used
   elsewhere on this card, e.g. Manage/Scan-history) so it costs nothing when
   a guest doesn't care to look, and a plain text filter appears once there
   are enough rows to actually be worth filtering. */
.menu-scan-recent-details{margin-top:4px;}
.menu-scan-recent-details summary{font-size:12px;font-weight:700;color:var(--ink-soft);cursor:pointer;list-style:none;}
.menu-scan-recent-details summary::-webkit-details-marker{display:none;}
.menu-scan-recent-details summary::before{content:'▸ ';}
.menu-scan-recent-details[open] summary::before{content:'▾ ';}
.menu-scan-recent-filters{display:flex;gap:6px;margin:8px 0;flex-wrap:wrap;}
.menu-scan-recent-filter{flex:1 1 140px;min-width:0;margin:0;padding:8px 10px;border:1.5px solid var(--line);border-radius:8px;font-family:inherit;font-size:12.5px;background:var(--paper);color:var(--ink);box-sizing:border-box;}
.menu-scan-recent-date-filter{flex:0 1 150px;min-width:0;margin:0;padding:8px 10px;border:1.5px solid var(--line);border-radius:8px;font-family:inherit;font-size:12.5px;background:var(--paper);color:var(--ink);box-sizing:border-box;}
.menu-scan-recent-filters #menuScanRecentClearBtn{width:auto;margin:0;padding:8px 10px;font-size:11.5px;flex-shrink:0;}
.menu-scan-recent-title{font-size:12px;font-weight:700;color:var(--ink-soft);margin:0 0 6px;}
.menu-scan-recent-row{display:flex;justify-content:space-between;align-items:center;gap:8px;padding:8px 10px;border:1px solid var(--line);border-radius:8px;margin:8px 0 0;font-size:12px;}
.menu-scan-recent-row button{width:auto;margin-top:0;padding:6px 10px;font-size:11.5px;flex-shrink:0;}
.menu-scan-recent-label{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;flex:1;min-width:0;}
.menu-scan-recent-date{color:var(--ink-soft);font-size:11px;flex-shrink:0;white-space:nowrap;}
.menu-scan-recent-empty{font-size:12.5px;color:var(--ink-soft);text-align:center;padding:8px 0 0;}
.preset-row{display:flex;flex-wrap:wrap;gap:6px;margin:8px 0 4px;}
.preset-chip{font-size:12px;padding:7px 12px;border-radius:999px;border:1.5px solid var(--line);background:var(--paper);color:var(--ink);cursor:pointer;font-family:inherit;}
.preset-chip:hover{border-color:var(--pine);}
footer{text-align:center;font-size:11px;color:#dfe9e2;padding:16px;}
footer a{color:#dfe9e2;}
/* Severity chips — a secondary, smaller chip row listing only whichever
   allergens/custom terms are currently selected, letting a guest flag any
   of them as severe/life-threatening without cluttering the primary
   selection grid above. Deliberately a distinct red (not the app's usual
   --caution tone) so it reads as "this one's different/more serious" even
   next to an avoid/trace badge elsewhere in the app. */
.severity-section{margin-top:18px;}
.severity-hint{font-size:12.5px;color:var(--ink-soft);line-height:1.5;margin:0 0 8px;}
.severity-chip{border:1.5px solid #b23a3a;background:var(--paper);color:#b23a3a;}
.severity-chip.active{background:#b23a3a;color:#fff;border-color:#b23a3a;}
.id-card-tag-severe{background:rgba(220,70,70,0.35) !important;border-color:rgba(255,140,140,0.7) !important;font-weight:700;}
/* Hasib: "are the emojis i have chosen slick, elegant and sexy for the
   card, be honest" -> "i want the new ones to have color too, can have
   color and be slick too, premium" — swapped the old flat emoji-text
   buttons for small coloured circular swatches: each button's own
   background is that glyph's muted jewel-tone colour (set inline per
   button, see renderIconGrid below — CSS here can't know each icon's
   colour ahead of time), with a fixed cream Tabler icon centred on top.
   The "active" state used to fill the whole button pine-green, which
   would now clash with/hide whichever colour that particular swatch is —
   drawn as a ring OUTSIDE the circle instead (a light gap, then a pine
   ring), so it reads as "selected" regardless of the swatch's own colour.
   A plain legacy-emoji chip (any card that hasn't been re-saved since
   this changed) still renders as before: the background style is simply
   omitted for those, falling back to this rule's own cream fill. */
.icon-picker{display:flex;flex-wrap:wrap;gap:10px;margin:10px 0 4px;}
.icon-chip{width:38px;height:38px;font-size:18px;line-height:1;padding:0;border-radius:50%;border:none;background:var(--cream);cursor:pointer;display:flex;align-items:center;justify-content:center;box-shadow:inset 0 0 0 1px rgba(0,0,0,0.12);}
.icon-chip i{color:#f6f1e4;font-size:18px;}
.icon-chip.active{box-shadow:inset 0 0 0 1px rgba(0,0,0,0.12),0 0 0 2px var(--paper),0 0 0 4px var(--pine);}
.my-cards-link{display:block;text-align:center;font-size:12.5px;color:var(--ink-soft);margin:0 0 20px;}
/* Parent/child handover (Hasib, Aug 2026) — deliberately calmer than
   .warn-box (this isn't a warning, just a status), and distinct from
   .success-banner too — a parent landing here should read this as "this is
   working as intended", not as an error state or a one-time celebration. */
.viewer-access-banner{display:flex;gap:10px;align-items:flex-start;border:1.5px solid var(--line);background:var(--paper);border-radius:12px;padding:12px 14px;margin:0 0 16px;font-size:12.5px;line-height:1.6;color:var(--ink);}
.viewer-access-icon{font-size:20px;line-height:1;flex-shrink:0;}
.trust-badge-banner{display:flex;gap:10px;align-items:flex-start;border:1.5px solid #bcd6c5;background:var(--safe-bg,#eef6ef);color:var(--safe,#3d7a4e);border-radius:12px;padding:12px 14px;margin:0 0 16px;font-size:12.5px;line-height:1.6;}
.trust-badge-icon{font-size:20px;line-height:1;flex-shrink:0;}
.trust-badge-banner strong{display:block;font-size:13px;color:var(--safe,#3d7a4e);}
.viewer-access-banner strong{display:block;font-size:13px;}
.my-cards-row{display:flex;align-items:center;gap:10px;border:1px solid var(--line);border-radius:10px;padding:10px 12px;margin-bottom:8px;background:var(--paper);}
.my-cards-icon{font-size:24px;flex-shrink:0;}
.my-cards-info{flex:1;min-width:0;}
.my-cards-name{font-size:13.5px;font-weight:700;margin:0;}
.my-cards-meta{font-size:12.5px;color:var(--ink-soft);margin:2px 0 0;}
.my-cards-actions{display:flex;gap:6px;flex-shrink:0;}
.my-cards-actions a,.my-cards-actions button{font-size:11.5px;padding:6px 10px;border-radius:8px;border:1px solid var(--line);background:var(--paper);color:var(--ink);text-decoration:none;cursor:pointer;font-family:inherit;}
.my-cards-actions a.primary-action{background:var(--pine);color:var(--cream);border-color:var(--pine);}
.my-cards-carousel{display:flex;overflow-x:auto;scroll-snap-type:x mandatory;gap:10px;padding:2px 2px 4px;-webkit-overflow-scrolling:touch;scrollbar-width:none;}
.my-cards-carousel::-webkit-scrollbar{display:none;}
.my-cards-carousel .my-cards-row{scroll-snap-align:center;flex:0 0 100%;min-width:100%;margin-bottom:0;}
.my-cards-dots{display:flex;justify-content:center;gap:6px;margin:8px 0 2px;}
.my-cards-dot{width:7px;height:7px;border-radius:50%;background:var(--line);border:none;padding:0;cursor:pointer;transition:background 0.2s var(--motion-standard, cubic-bezier(.2,.75,.25,1)),transform 0.2s var(--motion-standard, cubic-bezier(.2,.75,.25,1));}
.my-cards-dot.active{background:var(--pine);transform:scale(1.35);}
.my-cards-hint{text-align:center;font-size:12.5px;color:var(--ink-soft);margin:0 0 2px;}
@media (prefers-reduced-motion: reduce){.my-cards-carousel{scroll-behavior:auto;}.my-cards-dot{transition:none;}}
/* Family dashboard (Hasib, Aug 2026) — a client-only comparison view built
   entirely from the same rows already fetched for the carousel above; no
   new server endpoint and no server-side "these cards belong to one family"
   grouping, consistent with the rest of this page's privacy model. */
.my-cards-toolbar{display:flex;gap:8px;justify-content:center;flex-wrap:wrap;margin:0 0 14px;}
.my-cards-toolbar button{font-size:12.5px;padding:7px 14px;border-radius:8px;border:1.5px solid var(--pine);background:var(--paper);color:var(--pine);font-weight:700;cursor:pointer;font-family:inherit;}
.my-cards-toolbar button.caution-btn{border-color:var(--caution);color:var(--caution);}
.my-cards-nav{display:flex;align-items:center;justify-content:center;gap:10px;margin:8px 0 2px;}
.my-cards-nav .my-cards-dots{margin:0;}
.my-cards-nav-btn{width:28px;height:28px;flex-shrink:0;border-radius:50%;border:1px solid var(--line);background:var(--paper);color:var(--pine);font-size:16px;line-height:1;cursor:pointer;display:flex;align-items:center;justify-content:center;font-family:inherit;padding:0;}
.my-cards-nav-btn:active{transform:scale(0.92);}
.compare-banner{display:flex;align-items:center;gap:8px;border:1px solid var(--line);border-radius:10px;background:var(--trace-bg);color:var(--ink);padding:10px 12px;margin-bottom:12px;font-size:13px;line-height:1.5;}
.compare-banner-caution{background:var(--caution-bg);color:var(--caution);}
.compare-table-wrap{overflow-x:auto;margin-bottom:12px;}
.compare-table{width:100%;border-collapse:collapse;font-size:12.5px;}
.compare-table th,.compare-table td{padding:7px 8px;text-align:center;border-bottom:1px solid var(--line);}
.compare-table thead th{font-weight:700;font-size:11.5px;}
.compare-avatar{display:block;font-size:20px;margin-bottom:2px;}
.compare-rowlabel{text-align:left;font-weight:700;white-space:nowrap;}
.compare-shared td{background:var(--trace-bg);}
.compare-check{color:var(--safe);font-weight:700;}
.compare-dash{color:var(--ink-soft);}
.compare-actions{text-align:center;margin-bottom:10px;}
.compare-open-links{display:flex;flex-direction:column;gap:6px;}
.compare-open-links a{font-size:12.5px;padding:8px 10px;border-radius:8px;border:1px solid var(--line);background:var(--paper);color:var(--ink);text-decoration:none;text-align:center;}
/* Household safety sheet (Hasib, Aug 2026) — a print-only section that sits
   hidden inside this same interactive page, following the codebase's usual
   @media-print convention (see adminNetworkStatsPage's .print-header) rather
   than a dedicated route: it's built from data this device already legitimately
   has (each saved card's own cardId, same public trust bar as GET /card/:id/current
   itself), so there's nothing to gate that isn't already reachable by anyone
   holding that card's link. Rules are scoped under .my-cards-page so they can
   never leak into any other page sharing CARD_PAGE_STYLE. */
#myCardsSheet{display:none;}
.sheet-header{border-bottom:2px solid var(--pine);padding-bottom:10px;margin-bottom:16px;}
.sheet-header-brand{font-size:17px;font-weight:700;color:var(--ink);}
.sheet-header-meta{font-size:11.5px;color:var(--ink-soft);margin-top:3px;}
.sheet-child{position:relative;border:1px solid var(--line);border-radius:10px;padding:12px 90px 12px 14px;margin-bottom:10px;}
.sheet-child-head{display:flex;align-items:center;gap:8px;margin-bottom:4px;}
.sheet-child-icon{font-size:22px;}
.sheet-child-name{font-size:14px;font-weight:700;}
.sheet-allergens,.sheet-emergency,.sheet-epipen{font-size:12px;margin:3px 0;color:var(--ink);}
.sheet-epipen.expired{color:var(--caution);font-weight:700;}
.sheet-qr{position:absolute;top:12px;right:14px;width:70px;height:70px;}
.sheet-footer{text-align:center;font-size:10.5px;color:var(--ink-soft);margin-top:14px;}
@media print{
  .my-cards-page header,.my-cards-page footer,.my-cards-page #myCardsToolbar,.my-cards-page #myCardsList,.my-cards-page #myCardsCompare,.my-cards-page section > p.note{display:none !important;}
  .my-cards-page #myCardsSheet{display:block !important;}
  .my-cards-page{padding:0;background:#fff;}
  .sheet-child{break-inside:avoid;}
}
.scan-history-controls{display:flex;gap:8px;margin-top:10px;}
.scan-history-controls input[type=text]{flex:1;width:auto;margin-top:0;}
.scan-history-controls select{flex-shrink:0;padding:9px 10px;border:1.5px solid var(--line);border-radius:8px;font-size:13px;font-family:inherit;background:var(--paper);color:var(--ink);}
.scan-history-group-label{font-size:10.5px;font-weight:700;text-transform:uppercase;letter-spacing:.05em;color:var(--ink-soft);margin:16px 0 4px;}
.scan-history-group-label:first-child{margin-top:8px;}
.scan-history-row{display:flex;align-items:center;gap:10px;border-bottom:1px solid var(--line);padding:9px 2px;font-size:13px;}
.scan-history-row:last-child{border-bottom:none;}
.scan-history-icon{font-size:18px;flex-shrink:0;line-height:1;}
.scan-history-info{flex:1;min-width:0;}
.scan-history-name{font-weight:700;}
.scan-history-time{display:flex;flex-direction:column;align-items:flex-end;flex-shrink:0;}
.scan-history-when{color:var(--ink-soft);font-size:12.5px;white-space:nowrap;}
.scan-history-precise{color:var(--ink-soft);font-size:12.5px;white-space:nowrap;opacity:0.75;margin-top:1px;}
.scan-history-empty{text-align:center;padding:20px 10px;color:var(--ink-soft);font-size:12.5px;}
.scan-history-empty-icon{font-size:28px;display:block;margin-bottom:6px;}
.scan-history-loading{text-align:center;padding:16px 10px;color:var(--ink-soft);font-size:12.5px;}
.safe-places-dishes{font-size:12.5px;color:var(--ink-soft);margin-top:2px;}
.safe-places-area{font-size:12.5px;color:var(--ink-soft);margin-top:3px;font-weight:600;}
.safe-places-caveat{font-size:12.5px;color:var(--ink-soft);margin-top:3px;font-style:italic;}
.nearby-search-row{display:flex;flex-wrap:wrap;align-items:center;gap:10px;}
.nearby-search-divider{font-size:12.5px;color:var(--ink-soft);font-style:italic;}
.nearby-postcode-row{display:flex;gap:6px;}
.nearby-postcode-input{padding:9px 11px;font-size:13px;font-family:inherit;border:1.5px solid var(--line);border-radius:8px;background:var(--paper);color:var(--ink);width:130px;margin-top:0;}
.nearby-postcode-input:focus{outline:2px solid var(--pine);outline-offset:1px;}
.nearby-location-status{font-size:12.5px;color:var(--ink-soft);margin:6px 0 0;min-height:14px;}
.nearby-map{height:260px;border-radius:14px;border:1px solid var(--line);overflow:hidden;margin-top:10px;z-index:0;box-shadow:0 3px 12px rgba(13,74,55,0.08);}
.nearby-map .leaflet-control-attribution{font-size:8.5px;}
.nearby-map-popup .leaflet-popup-content-wrapper{border-radius:10px;box-shadow:0 4px 16px rgba(13,74,55,0.22);}
.nearby-map-popup .leaflet-popup-content{margin:9px 12px;font-family:var(--card-font-body);font-size:12.5px;line-height:1.5;}
.nearby-map-popup-name{font-weight:700;color:var(--ink);}
.nearby-map-popup-dist{color:var(--pine);font-weight:600;font-size:11.5px;}
.nearby-map-popup a{color:var(--pine);font-weight:600;text-decoration:none;}
.nearby-map-popup a:hover{text-decoration:underline;}
/* Hasib: "want the card more premium, expensive and slick... not just the
   card but the rest of the dashboard" — these collapsible sections used
   to be plain flat-line boxes; now a soft shadow + inner top highlight
   gives them the same quiet "resting card" elevation used across the rest
   of the app's premium surfaces (dish cards, search bar, etc.), and the
   chevron gets a colour + smooth rotation on open instead of a static
   glyph, so opening one feels like a considered interaction, not just a
   toggle. */
.card-section{border:1px solid var(--line);border-radius:14px;padding:16px 18px;margin:16px 0 22px;background:var(--paper);box-shadow:0 2px 10px rgba(28,51,43,0.07),inset 0 1px 0 rgba(255,255,255,0.7);}
.card-section summary{cursor:pointer;list-style:none;font-size:14px;font-weight:700;color:var(--ink);user-select:none;display:flex;align-items:center;gap:7px;}
.card-section summary::-webkit-details-marker{display:none;}
.card-section summary::before{content:'▸';color:var(--pine);display:inline-block;font-size:11px;transition:transform 0.2s var(--motion-entrance, cubic-bezier(.16,1,.3,1));}@media (prefers-reduced-motion: reduce){html .card-section summary::before{transition:none;}}
.card-section[open] summary::before{transform:rotate(90deg);}
.card-section[open] summary::before{content:'▾ ';}
.card-section[open] summary{margin-bottom:8px;}
.card-section .note{margin-top:0;}
.card-group-label{font-size:10.5px;text-transform:uppercase;letter-spacing:.06em;color:var(--ink-soft);margin:16px 0 6px 2px;}
.card-group-label:first-child{margin-top:0;}
.manage-group{margin-top:14px;}
.manage-group + .manage-group{border-top:1px solid var(--line);padding-top:16px;margin-top:20px;}
.practice-together-note{background:rgba(255,190,80,0.12);border:1px solid rgba(255,170,60,0.35);border-radius:8px;padding:8px 10px;font-size:12.5px;line-height:1.5;color:var(--ink);margin:0 0 10px;}
.phrase-cards-grid{display:flex;flex-direction:column;gap:6px;}
.phrase-card{border:1px solid var(--line);border-radius:8px;padding:8px 10px;background:var(--paper);}
.phrase-card-head{font-size:10px;text-transform:uppercase;letter-spacing:.05em;color:var(--ink-soft);font-weight:700;margin-bottom:3px;}
.phrase-card-body{font-size:12.5px;line-height:1.5;color:var(--ink);margin-bottom:2px;}
.phrase-card-body strong{color:var(--caution);}
.phrase-card-tag{display:inline-block;background:var(--cream);border-radius:5px;padding:1px 6px;margin:0 3px 3px 0;font-size:11.5px;}
.phrase-card-tag.severe{background:var(--caution-bg);color:var(--caution);font-weight:700;}
.phrase-card-note{font-size:12.5px;color:var(--ink-soft);line-height:1.4;}
.more-langs-toggle{margin-top:6px;}
.more-langs-toggle summary{cursor:pointer;font-size:12px;color:var(--ink-soft);text-decoration:underline;list-style:none;}
.more-langs-toggle summary::-webkit-details-marker{display:none;}
.more-langs-toggle .phrase-cards-grid{margin-top:6px;}
.more-icons-toggle{margin-top:6px;}
.more-icons-toggle summary{cursor:pointer;font-size:12px;color:var(--ink-soft);text-decoration:underline;list-style:none;}
.more-icons-toggle summary::-webkit-details-marker{display:none;}
.more-icons-toggle .icon-picker{margin-top:8px;}
.link-style-btn{background:none;border:none;padding:0;color:inherit;text-decoration:underline;font-family:inherit;font-size:inherit;cursor:pointer;}

/* ===========================================================================
   HIGHER CONTRAST WHEN THE DEVICE ASKS FOR IT
   ===========================================================================

   This stylesheet carried 23 prefers-reduced-motion blocks and ZERO
   prefers-contrast or prefers-color-scheme. Measured, not assumed. So the card
   honoured a comfort preference thoroughly and a legibility one not at all —
   on the page held at arm's length in daylight, by a stranger or by an older
   care worker, while the guest menu browsed at leisure before ordering has two
   explicit toggles.

   AN OS PREFERENCE RATHER THAN A TOGGLE, and the reason is structural, not
   effort. The guest page's toggles set .high-contrast on #app. On THIS page
   every modal — the emergency overlay above all — is a SIBLING of .app, not a
   descendant, and .app here carries no id at all. Lifting those toggles across
   would give the card a control that restyled the whole page except the one
   surface the argument is about, and would call getElementById('app') on an
   element that does not exist. See the report accompanying this change for what
   a real toggle would additionally need.

   WHAT THIS RAISES, chosen from measurement against the existing palette:
     --ink-soft   5.61:1 on paper. Passes AA, but it is the muted colour behind
                  every .note on the page at 12.5px. Under this preference the
                  card simply stops muting body text.
     --line       1.53:1 — well under the 3.0 a boundary needs. #8a7c5c is 4.03
                  and still reads as a hairline rather than a black rule.
     the semantic quartet — reused verbatim from the guest page's own
                  .high-contrast block rather than a second palette invented
                  here, so the two pages mean the same thing by "high contrast".
                  Each roughly doubles: safe 5.13 -> 8.23, caution 4.71 -> 8.21,
                  trace 5.36 -> 7.90, unknown 5.65 -> 13.31.

   WHAT IT DELIBERATELY DOES NOT TOUCH:
     --pine / --cream. The ID card face is already 12.98:1 and the white-on-pine
                  text 14.63:1. Flattening them to black and white would recolour
                  a designed artifact to fix a problem it does not have — and
                  prefers-contrast:more means "I need more contrast", not
                  "make it monochrome". That is forced-colors, which is a
                  different query and is mostly handled by the browser.
     .emergency-trigger-btn's own gradient. White on its lightest stop (#dd6a41)
                  is 3.38:1, under AA for 15px bold — noted and left alone on
                  Hasib's instruction. It is a large solid button nobody misses,
                  and it was designed.

   THE LIMIT, because a stylesheet cannot claim a rendered result:
   test_card_high_contrast.mjs asserts these declarations exist and that the
   values they set beat the ones they replace. It cannot claim the card reads
   well in sunlight. Nobody has measured that, and nothing here can.
   =========================================================================== */
@media (prefers-contrast: more) {
  :root {
    /* var(--ink), not the hex. test_card_page_css.mjs forbids a second
       hardcoded BRAND.color.ink outside :root and is right to — the point of
       this line is "stop muting body text", which is exactly what following
       the token says. */
    --ink-soft: var(--ink);
    --line: #8a7c5c;
    --safe: #005c1f;
    --caution: #a30000;
    --trace: #6b4c00;
    --unknown: #00305c;
  }
  /* Same move the guest page's high-contrast block makes: a boundary someone
     asked to see more of should be thicker as well as darker. */
  .card-section, .card-action-row button, .card-action-row a,
  .secondary, .primary, input, select, textarea {
    border-width: 2.5px;
  }
  /* THE EMERGENCY OVERLAY, which is the surface this whole block is for.
     Its secondary controls sit on a full-bleed gradient and are drawn with
     white at very low alpha — 0.08 and 0.10 backgrounds, 0.3 and 0.45 borders,
     a 0.55 label. Those are the weakest things on the screen a stranger reads
     during a reaction. Raised here rather than in the base rules so the
     designed look is unchanged for everyone who has not asked for this.
     Selectors mirror the originals exactly, including the .emergency-secondary
     ancestor, or specificity would leave them untouched. */
  .emergency-secondary-label { color: rgba(255,255,255,0.95); }
  .emergency-secondary .emergency-overlay-contact {
    background: rgba(255,255,255,0.22);
    border-color: rgba(255,255,255,0.95);
    border-width: 2px;
  }
  .emergency-secondary .emergency-overlay-identity,
  .emergency-secondary .emergency-overlay-parent-note {
    background: rgba(255,255,255,0.18);
    border-color: rgba(255,255,255,0.85);
    border-width: 2px;
  }
  .emergency-overlay-helpline { color: rgba(255,255,255,0.95); }
  .emergency-overlay-close { border-color: #ffffff; border-width: 2px; }
  .emergency-sos-locstatus { opacity: 1; }
  /* The allergen chips on the card face, same reasoning: white at 0.2 over the
     pine gradient is the lowest-contrast element on the card a stranger reads.
     A SEVERE CHIP IS DELIBERATELY UNAFFECTED, and not by accident:
     .id-card-tag-severe sets its red background and border with !important, so
     this rule cannot reach it and must not. That red is meaning-bearing — it is
     what still distinguishes a severe allergen when the icon font has not
     loaded, which is the offline case. Raising the neutral chips while leaving
     the red one alone increases the difference between them, which is the point.
     Verified in Chrome by applying this block's rules and re-reading the
     computed style: a plain chip goes 0.2 -> 0.32 and a severe chip does not
     move. */
  .id-card-tag { background: rgba(255,255,255,0.32); border-color: rgba(255,255,255,0.8); }
  /* The freshness pill's own border, which is what separates its three states
     for someone who cannot rely on the hue alone. */
  .offline-ready-badge, .freshness-pill { border-width: 2px; }
}
