/* =====================================================================
   TAG AND TAP - web app
   Same tokens as app/theme.ts. If you change one, change both.
   ===================================================================== */

@font-face{
  font-family:'Outfit';
  src:url('/assets/fonts/outfit-var.woff2') format('woff2-variations');
  font-weight:100 900; font-style:normal; font-display:swap;
}

:root{
  --bg:#F5F6F8; --surface:#FFFFFF; --alt:#ECEEF1; --sunk:#E4E7EA;
  --hair:rgba(16,19,23,.09); --hairUp:rgba(16,19,23,.15);
  --tx:#101317; --dim:#5C646D; --low:#626B74;
  --signal:#FF4D17; --signalText:#A8330B;
  --wash:rgba(255,77,23,.10); --edge:rgba(255,77,23,.26); --onSig:#160702;
  --live:#0F6E4A; --liveWash:rgba(15,110,74,.10);
  --warn:#8A5A00; --warnWash:rgba(138,90,0,.10);
  --danger:#C4271F; --dangerWash:rgba(196,39,31,.08);
  --f:'Outfit',system-ui,-apple-system,'Segoe UI',sans-serif;
  --ease:cubic-bezier(.22,1,.36,1);
  --r-chip:8px; --r-thumb:14px; --r-card:15px; --r-sheet:22px;
}

*,*::before,*::after{box-sizing:border-box;-webkit-tap-highlight-color:transparent}
html{
  -webkit-text-size-adjust:100%;
  /* Reserve the scrollbar's width whether or not one is showing.
     Without this, a tall screen (Home) has a scrollbar and a short one
     (Tags) does not, the viewport width changes by ~15px between them,
     and everything centred - the logo, the tab bar, the whole column -
     jumps sideways by half that on every navigation. */
  scrollbar-gutter:stable;
}
body{
  margin:0;background:var(--bg);color:var(--tx);
  font-family:var(--f);font-size:16px;line-height:1.5;font-weight:400;
  -webkit-font-smoothing:antialiased;
  overscroll-behavior-y:none;
}
img{max-width:100%;display:block}
/* An author `display` beats the UA rule for [hidden], and that has
   silently un-hidden things here before. Settle it once. */
[hidden]{display:none!important}
button,input,textarea,select{font:inherit;color:inherit}
button{cursor:pointer;border:0;background:none}

/* Anchors are used as buttons, cards and rows throughout. Underlining
   them is the browser default and it looks like a mistake on a filled
   button, so it is switched off here and re-enabled only on real inline
   links inside body copy. */
a{color:inherit;text-decoration:none}
.sm a,.lead a,p a{text-decoration:underline;text-underline-offset:2px}

/* ── shell ──────────────────────────────────────────────── */
.app{
  /* Named so the transition animates THIS and not the whole page. With
     `root` animating, the fixed header and tab bar slid along with the
     content - which is why the logo drifted forward on a push and back
     on a pop, but held still on a tab swap. */
  max-width:480px;margin:0 auto;min-height:100dvh;
  display:flex;flex-direction:column;position:relative;
  background:var(--bg);
}
.view{
  flex:1;padding:0 22px calc(28px + env(safe-area-inset-bottom));
  padding-top:calc(14px + env(safe-area-inset-top));
}
body.chrome .view{padding-top:calc(82px + env(safe-area-inset-top))}
.view.has-tabs{padding-bottom:calc(96px + env(safe-area-inset-bottom))}
.view.has-foot{padding-bottom:calc(102px + env(safe-area-inset-bottom))}

/* entrance — mirrors Appear() in the native app */
@keyframes rise{from{opacity:0;transform:translateY(14px)}to{opacity:1;transform:none}}
.st{opacity:1;animation:rise .30s var(--ease) backwards}
.st:nth-child(1){animation-delay:0ms}   .st:nth-child(2){animation-delay:45ms}
.st:nth-child(3){animation-delay:90ms}  .st:nth-child(4){animation-delay:135ms}
.st:nth-child(5){animation-delay:180ms} .st:nth-child(6){animation-delay:225ms}
.st:nth-child(n+7){animation-delay:270ms}

/* ── boot ───────────────────────────────────────────────── */
.boot{min-height:100dvh;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:18px}
.boot-mark{
  width:56px;height:56px;border-radius:99px;background:var(--signal);
  animation:breathe 1.4s ease-in-out infinite;
}
@keyframes breathe{0%,100%{opacity:.35;transform:scale(.92)}50%{opacity:1;transform:scale(1)}}
.boot-text{color:var(--dim);font-size:14px;margin:0}

/* ── type ───────────────────────────────────────────────── */
h1{font-weight:800;font-size:34px;letter-spacing:-.038em;line-height:1.08;margin:4px 0 0}
h2{font-weight:800;font-size:27px;letter-spacing:-.034em;line-height:1.12;margin:4px 0 0}
h3{font-weight:700;font-size:19px;letter-spacing:-.026em;margin:0}
.greet{font-size:14px;color:var(--dim);margin:0}
.lead{font-size:16px;color:var(--dim);line-height:1.6;margin:12px 0 0}
.sm{font-size:14px;color:var(--dim);line-height:1.55}
.lbl{
  font-size:11.5px;font-weight:600;letter-spacing:.09em;
  text-transform:uppercase;color:var(--low);
}
.code{font-size:13px;font-weight:600;letter-spacing:.05em;color:var(--low)}
.sect{display:flex;justify-content:space-between;align-items:center;margin:38px 0 16px}
.sect .r{font-size:11.5px;font-weight:600;letter-spacing:.09em;text-transform:uppercase;color:var(--dim)}

/* ── cards / rows ───────────────────────────────────────── */
.card{background:var(--surface);border:1px solid var(--hair);border-radius:var(--r-card);padding:22px}
.card.flush{padding:0 20px}
.card.alert{background:var(--wash);border-color:var(--edge)}

.row{
  display:flex;align-items:center;justify-content:space-between;gap:16px;
  min-height:60px;padding:16px 0;border-bottom:1px solid var(--hair);
  width:100%;text-align:left;
}
.row:last-child{border-bottom:0}
.row .v{font-size:14px;color:var(--dim);display:flex;align-items:center;gap:8px;min-width:0}
.row .v span{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.row.danger{color:var(--danger)}
.chev{color:var(--dim);flex:0 0 auto}

/* ── tag card ───────────────────────────────────────────── */
.tag{
  display:flex;gap:15px;align-items:center;padding:14px;margin-bottom:12px;width:100%;
  background:var(--surface);border:1px solid var(--hair);border-radius:var(--r-card);
  text-align:left;transition:transform .13s var(--ease),opacity .13s;
}
.tag:active{transform:scale(.975);opacity:.92}
.tag.lost{border-color:var(--edge);background:var(--wash)}
.tag.add{justify-content:center;gap:10px;border-style:dashed;background:none;padding:20px}
.tag.add .plus{color:var(--signalText);font-size:20px;font-weight:600;line-height:1}
.tag.add .nm{color:var(--dim);font-size:15px}
.tag .mid{flex:1;min-width:0}
.nm{font-weight:600;font-size:16.5px;letter-spacing:-.014em;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.sub{
  font-size:11.5px;font-weight:600;letter-spacing:.09em;text-transform:uppercase;
  color:var(--low);margin-top:7px;display:flex;align-items:center;gap:8px;
}
.dot{width:6px;height:6px;border-radius:9px;flex:0 0 6px;background:var(--live);display:block}
.dot.l{background:var(--signal)}

/* ── avatar ─────────────────────────────────────────────── */
.ph{
  width:56px;height:56px;border-radius:var(--r-thumb);flex:0 0 56px;
  display:grid;place-items:center;overflow:hidden;
  background:var(--alt) center/cover no-repeat;border:1px solid var(--hair);
  font-size:26px;line-height:1;
}
.ph.lg{width:96px;height:96px;border-radius:99px;font-size:44px;flex:0 0 96px}
.ph.xl{width:112px;height:112px;border-radius:99px;font-size:52px;flex:0 0 112px}
/* display:grid makes these block boxes, so text-align on the parent can
   never centre them - which is why the photo sat hard left under
   centred text. */
.ph.lg,.ph.xl{margin-inline:auto}

/* ── pills ──────────────────────────────────────────────── */
.pill{
  display:inline-block;padding:4px 10px;border-radius:99px;border:1px solid;
  font-size:11px;font-weight:700;letter-spacing:.08em;text-transform:uppercase;
}
.pill.active{color:var(--live);background:var(--liveWash);border-color:rgba(15,110,74,.28)}
.pill.lost{color:var(--signalText);background:var(--wash);border-color:var(--edge)}
.pill.unassigned{color:var(--warn);background:var(--warnWash);border-color:rgba(138,90,0,.24)}
.pill.disabled{color:var(--dim);background:var(--alt);border-color:var(--hair)}
.flag{
  padding:5px 10px;border-radius:var(--r-chip);background:var(--signal);color:var(--onSig);
  font-size:10.5px;font-weight:700;letter-spacing:.08em;flex:0 0 auto;
}

/* ── buttons ────────────────────────────────────────────── */
.btn{
  display:flex;align-items:center;justify-content:center;gap:10px;width:100%;
  min-height:56px;padding:16px 22px;border-radius:var(--r-card);
  border:1px solid transparent;font-weight:600;font-size:15px;letter-spacing:-.012em;
  margin-bottom:11px;transition:transform .13s var(--ease),opacity .13s;
}
.btn:active:not(:disabled){transform:scale(.975);opacity:.92}
.btn:disabled{opacity:.45}
.btn.p{background:var(--tx);color:#fff}
.btn.s{background:var(--surface);color:var(--tx);border-color:var(--hairUp)}
.btn.g{background:transparent;color:var(--tx);border-color:var(--hairUp)}
.btn.sig{background:var(--signal);color:var(--onSig);font-weight:700}
.btn.d{background:var(--dangerWash);color:var(--danger);border-color:rgba(196,39,31,.32)}
.btn em{font-style:normal;margin-left:auto;font-size:13px;font-weight:600;opacity:.68}
.btn .spin{
  width:18px;height:18px;border-radius:99px;border:2px solid currentColor;
  border-right-color:transparent;animation:spin .7s linear infinite;
}
@keyframes spin{to{transform:rotate(360deg)}}

/* ── fields ─────────────────────────────────────────────── */
.fld{margin-bottom:22px}
.fld label{display:block;margin-bottom:9px}
.fld input,.fld textarea,.fld select{
  width:100%;background:var(--surface);border:1px solid var(--hairUp);
  border-radius:11px;padding:15px 14px;font-size:16px;
  transition:border-color .18s,box-shadow .18s;
}
.fld textarea{min-height:110px;resize:vertical;line-height:1.5}
.fld input:focus,.fld textarea:focus,.fld select:focus{
  outline:none;border-color:var(--signal);box-shadow:0 0 0 3px rgba(255,77,23,.14);
}
.fld input[aria-invalid="true"],.fld textarea[aria-invalid="true"]{border-color:var(--danger)}
.fld .err{color:var(--danger);font-size:13.5px;margin-top:8px;font-weight:500}
.fld .hint{color:var(--low);font-size:13.5px;margin-top:8px;line-height:1.45}

.fldrow{display:flex;gap:16px;align-items:flex-start;padding:18px 0;border-top:1px solid var(--hair)}
.fldrow:first-child{border-top:0}
.fldrow .fld{flex:1;margin:0;min-width:0}
.tgwrap{padding-top:30px;flex:0 0 46px}

/* display:block is load-bearing: width/height are ignored on an inline
   element, so without it the switch collapses to its knob. */
.tg{
  display:block;width:46px;height:28px;border-radius:99px;padding:2px;flex:0 0 46px;
  background:var(--sunk);border:1px solid var(--hairUp);
  transition:background .22s var(--ease),border-color .22s var(--ease);
}
.tg b{
  display:block;width:20px;height:20px;border-radius:99px;background:var(--dim);
  transition:transform .3s cubic-bezier(.34,1.4,.64,1),background .22s;
}
.tg[aria-checked="true"]{background:var(--wash);border-color:var(--edge)}
.tg[aria-checked="true"] b{transform:translateX(18px);background:var(--signal)}

.check{display:flex;gap:14px;align-items:flex-start;margin-bottom:18px}
.check .sm{flex:1}
.check a{color:var(--tx)}

/* ── banners ────────────────────────────────────────────── */
.ban{border-radius:11px;border:1px solid var(--hair);background:var(--surface);
  padding:18px;margin-bottom:22px;font-size:14.5px;line-height:1.55}
.ban.a{background:var(--wash);border-color:var(--edge)}
.ban.k{background:var(--liveWash);border-color:rgba(15,110,74,.26)}
.ban.w{background:var(--warnWash);border-color:rgba(138,90,0,.24)}
.ban b{font-weight:600}

/* ── NFC ────────────────────────────────────────────────── */
.nfc{display:flex;flex-direction:column;align-items:center;text-align:center;padding:36px 0 0}
.rings{position:relative;width:200px;height:200px;display:grid;place-items:center}
.rings i{
  position:absolute;inset:0;border-radius:99px;border:1px solid rgba(255,77,23,.34);
  animation:pulse 2.6s cubic-bezier(.2,.6,.3,1) infinite;opacity:0;
}
.rings i:nth-child(2){animation-delay:.85s}
.rings i:nth-child(3){animation-delay:1.7s}
@keyframes pulse{0%{transform:scale(.42);opacity:0}22%{opacity:1}100%{transform:scale(1);opacity:0}}
.core{width:72px;height:72px;border-radius:99px;background:var(--signal);box-shadow:0 0 54px rgba(255,77,23,.42)}
.rings.busy i{animation-duration:1.3s}
.rings.ok .core{background:var(--live);box-shadow:0 0 54px rgba(15,110,74,.4)}
.rings.ok i{animation:none;opacity:0}

/* ── success tick ───────────────────────────────────────── */
@keyframes pop{0%{transform:scale(0);opacity:0}55%{transform:scale(1.12);opacity:1}100%{transform:scale(1)}}
.tick{
  width:76px;height:76px;border-radius:99px;background:var(--signal);margin:0 auto;
  display:grid;place-items:center;animation:pop .5s cubic-bezier(.34,1.56,.64,1) both;
}
.tick svg{width:36px;height:36px}
.tick path{
  stroke:var(--onSig);stroke-width:3.4;fill:none;stroke-linecap:round;stroke-linejoin:round;
  stroke-dasharray:32;stroke-dashoffset:32;animation:draw .34s .24s var(--ease) forwards;
}
@keyframes draw{to{stroke-dashoffset:0}}

/* ── skeleton ───────────────────────────────────────────── */
@keyframes shimmer{0%,100%{opacity:.34}50%{opacity:.74}}
.sk{background:var(--alt);border-radius:var(--r-chip);animation:shimmer 1.8s ease-in-out infinite}

/* ── tabs ───────────────────────────────────────────────── */
.tabs{
  position:fixed;z-index:20;
  /* FLOATS off the screen edge. A bar welded to the bottom can only ever
     be a strip; lifting it away is what lets content pass UNDER the
     glass, which is the whole effect. */
  left:0;right:0;margin:0 auto;
  bottom:calc(10px + env(safe-area-inset-bottom));
  width:calc(100% - 24px);max-width:452px;
  display:grid;grid-template-columns:repeat(5,1fr);
  padding:11px 8px 12px;
  border-radius:32px;

  /* Thin enough to genuinely see through. The old 68% opaque fill meant
     the blur had almost nothing to work with, so it read as flat white. */
  background:
    linear-gradient(180deg, rgba(255,255,255,.30), rgba(255,255,255,.02) 50%,
                    rgba(255,255,255,.12)),
    color-mix(in srgb, var(--surface) 6%, transparent);
  /* Thin body, hard blur, heavy saturation: the colour of whatever is
     behind it survives, which is what separates glass from frosting. */
  backdrop-filter:blur(44px) saturate(3.2) brightness(1.06);
  -webkit-backdrop-filter:blur(44px) saturate(3.2) brightness(1.06);

  /* Edge light: a bright rim on top, a grounded one underneath, and a
     wide soft shadow so it reads as floating rather than pasted on. */
  box-shadow:
    inset 0 1.5px 0 rgba(255,255,255,1),          /* specular top edge  */
    inset 0 -1.5px 1px rgba(255,255,255,.60),     /* light bounced up   */
    inset 0 0 0 1px rgba(255,255,255,.55),        /* refracted rim      */
    0 20px 44px -12px rgba(16,19,23,.34),
    0 5px 14px -6px rgba(16,19,23,.16);
  border:0;
}
/* The old full-width bar drew its own hairline; a floating one must not. */

/* The bar's own lens edge - shallower than the pill's, because a large
   pane of glass curves less at its rim than a small bead does. */
.tabs::before{
  content:"";position:absolute;inset:0;border-radius:inherit;
  padding:5px;pointer-events:none;z-index:0;
  backdrop-filter:blur(1px) saturate(3.2) brightness(1.14);
  -webkit-backdrop-filter:blur(1px) saturate(3.2) brightness(1.14);
  -webkit-mask:linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
          mask:linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
  -webkit-mask-composite:xor;
          mask-composite:exclude;
}

/* The rim. A conic gradient masked to the border only: bright where the
   light lands, dark on the far side, so the edge reads as thickness
   rather than as a drawn outline. */
.tabs::after{
  content:"";position:absolute;inset:0;border-radius:inherit;
  padding:1.5px;pointer-events:none;
  background:conic-gradient(from 215deg,
    rgba(255,255,255,.98), rgba(255,255,255,.30) 18%,
    rgba(255,255,255,.95) 44%, rgba(16,19,23,.10) 60%,
    rgba(255,255,255,.42) 78%, rgba(255,255,255,.98));
  -webkit-mask:linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
          mask:linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
  -webkit-mask-composite:xor;
          mask-composite:exclude;
}

@supports not (backdrop-filter:blur(1px)){
  .tabs{
    background:linear-gradient(180deg,#fff,rgba(255,255,255,.94));
    box-shadow:0 14px 34px -10px rgba(16,19,23,.26), inset 0 0 0 1px var(--hair);
  }
}

/* The travelling marker. */
.tabpill{
  position:absolute;pointer-events:none;

  /* The bar has 8px of side padding, so the five columns share
     (100% - 16px), NOT 100%. Using 20% of the full width drifted the
     marker further off its icon with every column. */
  --col:calc((100% - 16px) / 5);

  /* Wraps the WHOLE item - icon AND label. A lens that covers only the
     icon leaves the word sitting outside it, which reads as a
     misaligned dot rather than a selected tab. */
  width:calc(var(--col) - 4px);
  height:58px;
  top:6px;
  /* Parked at column 0; JS sets width and translate in PIXELS.
     A percentage inside transform:translate() resolves against the
     element's OWN width, not the parent - so a calc() holding 100% moved
     the pill about a tenth of the distance it should have. Pixels from
     JS avoid that, and transform still keeps the move on the compositor
     rather than re-running layout and re-blurring every frame. */
  left:10px;
  border-radius:99px;

  /* Glass is three things stacked: what shows through (backdrop blur +
     saturation), a body tint, and the light that catches its edges. A
     flat translucent fill has none of the last part and just looks
     grey. */
  background:
    linear-gradient(180deg, rgba(255,255,255,.58), rgba(255,255,255,.14) 52%,
                    rgba(255,255,255,.34)),
    color-mix(in srgb, var(--tx) 5%, transparent);
  backdrop-filter:blur(22px) saturate(2.8) brightness(1.10);
  -webkit-backdrop-filter:blur(22px) saturate(2.8) brightness(1.10);
  box-shadow:
    inset 0 1px 0 rgba(255,255,255,.95),        /* specular top edge   */
    inset 0 -1px 0 rgba(16,19,23,.05),          /* grounded bottom     */
    inset 0 0 0 1px rgba(255,255,255,.45),      /* refracted rim       */
    0 4px 12px -4px rgba(16,19,23,.20),         /* lift off the bar    */
    0 1px 2px rgba(16,19,23,.05);
  /* A stable element now, so the browser interpolates this directly.
     Smoother than a snapshot morph, and nothing to rasterise. */
  /* A touch of overshoot, not a wobble. 1.42 sprang past the target and
     bounced back, which reads as loose rather than liquid. */
  /* transform only: the compositor moves an already-rasterised layer
     instead of the main thread redoing layout and blur each frame. */
  transition:transform .30s cubic-bezier(.32,1.06,.4,1),
             opacity .2s ease;
  will-change:transform;
}
/* THE LENS EDGE.
   A ring, masked to the border box, carrying a DIFFERENT backdrop filter
   from the body: almost no blur, strongly saturated and brightened. What
   is behind stays legible at the rim while the centre stays frosted, and
   that mismatch is what reads as a curved edge bending the light. */
.tabpill::before{
  content:"";position:absolute;inset:0;border-radius:inherit;
  padding:7px;pointer-events:none;
  backdrop-filter:blur(.5px) saturate(3.6) brightness(1.18) contrast(1.06);
  -webkit-backdrop-filter:blur(.5px) saturate(3.6) brightness(1.18) contrast(1.06);
  -webkit-mask:linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
          mask:linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
  -webkit-mask-composite:xor;
          mask-composite:exclude;
}

/* The specular rim sits on top of the lensing ring. */
.tabpill::after{
  content:"";position:absolute;inset:0;border-radius:inherit;
  padding:1.4px;pointer-events:none;
  background:conic-gradient(from 35deg,
    rgba(255,255,255,1), rgba(255,255,255,.35) 22%,
    rgba(255,255,255,.9) 50%, rgba(16,19,23,.08) 66%,
    rgba(255,255,255,.5) 84%, rgba(255,255,255,1));
  -webkit-mask:linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
          mask:linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
  -webkit-mask-composite:xor;
          mask-composite:exclude;
}
/* Without a backdrop filter there is nothing to refract, so lean on the
   tint instead of shipping an invisible shape. */
@supports not (backdrop-filter:blur(1px)){
  .tabpill{
    background:linear-gradient(180deg, rgba(255,255,255,.9), rgba(255,255,255,.5)),
               color-mix(in srgb, var(--tx) 10%, transparent);
  }
}
@media (prefers-reduced-motion:reduce){
  .tabpill{transition:none}
}
.tab{display:flex;flex-direction:column;align-items:center;gap:7px;color:var(--low);padding:0}
.tab{position:relative;z-index:2;transition:color .24s var(--ease)}
.tab .pip{display:none}
.tab[aria-current="page"]{color:var(--signalText)}
.tab[aria-current="page"] svg{transform:translateY(-1px)}
.tab svg{transition:transform .32s cubic-bezier(.34,1.32,.5,1)}
.tab:active svg{transform:scale(.88)}
.tab.act{justify-content:center}
.tab .disc{
  width:46px;height:46px;border-radius:99px;
  display:grid;place-items:center;
  background:linear-gradient(180deg,#FF6437,var(--signal));
  box-shadow:
    inset 0 1px 0 rgba(255,255,255,.45),
    0 6px 16px -4px rgba(255,77,23,.55),
    0 2px 4px rgba(16,19,23,.16);
  transition:transform .18s var(--ease);
}
.tab.act:active .disc{transform:scale(.9)}
.tab:active .disc{transform:scale(.93)}
.tab span:last-child{font-size:10.5px;font-weight:600;letter-spacing:.08em;text-transform:uppercase}

/* ── sticky footer action ───────────────────────────────── */
.foot{
  position:fixed;bottom:0;left:0;right:0;z-index:20;max-width:480px;margin:0 auto;
  background:var(--bg);border-top:1px solid var(--hair);
  padding:14px 22px calc(14px + env(safe-area-inset-bottom));
}
.foot .btn{margin-bottom:0}

/* ── header ─────────────────────────────────────────────── */
.hdr{display:flex;align-items:center;gap:14px;padding:6px 0 18px}
.back{
  width:42px;height:42px;border-radius:99px;display:grid;place-items:center;
  background:var(--surface);border:1px solid var(--hair);flex:0 0 42px;
}
.hdr h3{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}

/* ── toast ──────────────────────────────────────────────── */
.toast{
  position:fixed;left:50%;bottom:calc(120px + env(safe-area-inset-bottom));
  transform:translate(-50%,14px);z-index:60;
  background:var(--tx);color:#fff;padding:14px 20px;border-radius:12px;
  font-size:14.5px;font-weight:500;max-width:min(420px,88vw);text-align:center;
  opacity:0;pointer-events:none;transition:opacity .22s,transform .22s var(--ease);
  box-shadow:0 12px 30px -12px rgba(16,19,23,.5);
}
.toast.on{opacity:1;transform:translate(-50%,0)}
.toast.bad{background:var(--danger)}
.toast.good{background:var(--live)}

/* ── misc ───────────────────────────────────────────────── */
.empty{text-align:center;padding:60px 20px}
.empty h3{margin-bottom:10px}
.center{text-align:center}
.rows{margin:0}
.rows>div{display:flex;justify-content:space-between;gap:16px;padding:14px 0;border-bottom:1px solid var(--hair)}
.rows>div:last-child{border-bottom:0}
.rows dt{font-size:11.5px;font-weight:600;letter-spacing:.09em;text-transform:uppercase;color:var(--low);padding-top:3px}
.rows dd{margin:0;text-align:right;font-weight:500;font-size:14.5px;overflow-wrap:anywhere}
.qrbox{background:#fff;border-radius:var(--r-card);padding:20px;margin:0 auto 20px;width:78%;aspect-ratio:1}
.qrbox img{width:100%;height:100%;object-fit:contain}
.linkrow{display:flex;gap:10px;align-items:center;justify-content:center;margin-bottom:20px}

@media (prefers-reduced-motion:reduce){
  *{animation-duration:.01ms !important;animation-iteration-count:1 !important;
    transition-duration:.01ms !important}
}

/* =====================================================================
   Brand entrance
   ---------------------------------------------------------------------
   The mark springs in, three NFC waves radiate out of it exactly once,
   then the wordmark wipes in behind them. It runs ONCE on the welcome
   screen and never again - an animation you see on every screen stops
   being special by the third time.

   Total 1.5s to the last button. Long enough to feel composed, short
   enough that a returning user is never waiting on it (they land on
   /home and never see this screen).
   ===================================================================== */
.brand{display:flex;flex-direction:column;align-items:center;position:relative}

.brand-mark-wrap{position:relative;display:grid;place-items:center;width:200px;height:200px}

.brand-mark{
  width:112px;height:auto;position:relative;z-index:2;
  animation:markIn .8s cubic-bezier(.34,1.42,.62,1) both;
}
@keyframes markIn{
  0%  {opacity:0;transform:translateY(18px) scale(.68)}
  55% {opacity:1}
  100%{opacity:1;transform:none}
}

/* Waves that echo the ones drawn inside the mark itself. */
.brand-rings{position:absolute;inset:0;display:grid;place-items:center;z-index:1}
.brand-rings i{
  position:absolute;width:200px;height:200px;border-radius:99px;
  border:1.5px solid rgba(255,77,23,.42);opacity:0;
  animation:brandWave 1.5s cubic-bezier(.2,.6,.3,1) both;
}
.brand-rings i:nth-child(1){animation-delay:.30s}
.brand-rings i:nth-child(2){animation-delay:.46s}
.brand-rings i:nth-child(3){animation-delay:.62s}
@keyframes brandWave{
  0%  {transform:scale(.36);opacity:0}
  18% {opacity:.9}
  100%{transform:scale(1.05);opacity:0}
}

.brand-word{
  height:38px;width:auto;margin-top:22px;
  animation:wordIn .8s .52s cubic-bezier(.22,1,.36,1) both;
}
@keyframes wordIn{
  0%  {opacity:0;transform:translateY(10px);clip-path:inset(0 100% 0 0)}
  100%{opacity:1;transform:none;clip-path:inset(0 0 0 0)}
}

/* The copy follows the brand, not the other way round. */
.brand-after{animation:rise .6s .95s cubic-bezier(.22,1,.36,1) both}
.brand-actions{animation:rise .6s 1.15s cubic-bezier(.22,1,.36,1) both}

@media (prefers-reduced-motion:reduce){
  .brand-mark,.brand-word,.brand-after,.brand-actions{animation:none;opacity:1;
    transform:none;clip-path:none}
  .brand-rings{display:none}
}

/* Boot splash uses the mark too, so the first paint is already on-brand. */
.boot-mark{
  width:72px;height:72px;border-radius:0;background:none;
  background-image:url('/assets/img/logo-mark.png');
  background-size:contain;background-repeat:no-repeat;background-position:center;
}

/* =====================================================================
   Auth screens
   ---------------------------------------------------------------------
   Just the mark, never the wordmark: the word is already in the heading
   and repeating it makes the screen shout. The form lives in a white
   card on the faintly grey page, which is the whole reason the page is
   not pure white.
   ===================================================================== */
.auth{display:flex;flex-direction:column;min-height:calc(100dvh - 40px);padding-bottom:20px}

.auth-top{text-align:center;padding:22px 0 30px}
.auth-mark{
  width:76px;height:auto;margin:0 auto 24px;
  animation:markIn .72s cubic-bezier(.34,1.42,.62,1) both;
}
.auth-top h1{
  font-size:30px;letter-spacing:-.036em;
  animation:rise .55s .16s var(--ease) both;
}
.auth-top p{
  margin:12px auto 0;max-width:30ch;color:var(--dim);font-size:15.5px;line-height:1.55;
  animation:rise .55s .24s var(--ease) both;
}

.auth-card{
  background:var(--surface);border:1px solid var(--hair);
  border-radius:var(--r-sheet);padding:26px 22px 24px;
  box-shadow:0 20px 40px -28px rgba(16,19,23,.28);
  animation:rise .6s .32s var(--ease) both;
}
.auth-foot{
  text-align:center;margin-top:26px;font-size:14.5px;color:var(--dim);
  animation:rise .55s .46s var(--ease) both;
}
.auth-foot a{color:var(--tx);font-weight:600;display:inline-block;padding:10px 4px}

/* ── floating label field ───────────────────────────────── */
.ff{position:relative;margin-bottom:16px}
.ff input{
  width:100%;height:64px;padding:26px 16px 10px;
  background:var(--bg);border:1.5px solid transparent;border-radius:14px;
  font-size:16px;font-weight:500;
  transition:border-color .18s,background .18s,box-shadow .18s;
}
.ff.has-btn input{padding-right:56px}
.ff label{
  position:absolute;left:16px;top:21px;pointer-events:none;
  color:var(--dim);font-size:16px;font-weight:400;
  transform-origin:left top;
  transition:transform .18s var(--ease),color .18s;
}
/* :placeholder-shown is why every input carries placeholder=" " */
.ff input:focus + label,
.ff input:not(:placeholder-shown) + label{
  transform:translateY(-12px) scale(.72);
  color:var(--low);font-weight:600;letter-spacing:.04em;
}
.ff input:focus{
  outline:none;background:var(--surface);border-color:var(--signal);
  box-shadow:0 0 0 4px rgba(255,77,23,.12);
}
.ff input:focus + label{color:var(--signalText)}
.ff input[aria-invalid="true"]{border-color:var(--danger);background:rgba(196,39,31,.03)}
.ff .err{color:var(--danger);font-size:13px;margin:8px 0 0 2px;font-weight:500}
.ff .peek{
  /* Centred on the INPUT (64px tall), not on .ff - the wrapper also
     holds the strength meter and hint, so a 50% offset sat it low. */
  position:absolute;right:9px;top:32px;transform:translateY(-50%);
  width:46px;height:46px;border-radius:99px;display:grid;place-items:center;
  color:var(--dim);
}
.ff .peek:active{background:var(--alt)}

/* ── terms row ──────────────────────────────────────────── */
.auth-check{display:flex;gap:14px;align-items:flex-start;margin:20px 0 4px}
.auth-check p{margin:0;font-size:14px;color:var(--dim);line-height:1.5;flex:1}
.auth-check a{color:var(--tx);font-weight:600;text-decoration:underline;text-underline-offset:2px}

/* ── strength meter ─────────────────────────────────────── */
.meter{display:flex;gap:5px;margin:10px 0 0}
.meter i{
  flex:1;height:4px;border-radius:9px;background:var(--sunk);
  transition:background .25s var(--ease);
}
.meter.s1 i:nth-child(1){background:var(--danger)}
.meter.s2 i:nth-child(-n+2){background:var(--warn)}
.meter.s3 i:nth-child(-n+3){background:var(--live)}
.meter.s4 i{background:var(--live)}
.meter-text{font-size:12.5px;color:var(--low);margin:8px 0 0}

@media (prefers-reduced-motion:reduce){
  .auth-mark,.auth-top h1,.auth-top p,.auth-card,.auth-foot{animation:none}
}

/* =====================================================================
   HOME - app bar, hero, tag cards, activity feed
   ---------------------------------------------------------------------
   The screen carries ONE bold element - the dark hero - and keeps
   everything under it quiet. Two competing blocks is what made the
   earlier version read as busy rather than considered.
   ===================================================================== */

/* -- app bar ---------------------------------------------- */
.bar{
  /* Fixed, not sticky: the element now lives after #app in the DOM (so
     the router cannot destroy it), and a sticky box would simply flow to
     the bottom of the document. */
  position:fixed;top:0;left:0;right:0;margin:0 auto;
  max-width:480px;z-index:30;
  display:flex;align-items:center;justify-content:space-between;
  padding:calc(10px + env(safe-area-inset-top)) 22px 10px;
  background:color-mix(in srgb, var(--bg) 82%, transparent);
  backdrop-filter:saturate(1.6) blur(14px);
  -webkit-backdrop-filter:saturate(1.6) blur(14px);
}
.bar-mark{
  width:54px;height:54px;flex:0 0 54px;
  background:url("/assets/img/logo-mark.png") center/contain no-repeat;
  transition:transform .22s var(--ease);
}
.bar-mark:active{transform:scale(.92)}
.bar-act{
  position:relative;width:38px;height:38px;border-radius:99px;
  display:grid;place-items:center;color:var(--tx);
  transition:background .2s var(--ease),transform .2s var(--ease);
}
.bar-act:active{transform:scale(.9);background:var(--alt)}
.bar-act .badge{
  position:absolute;top:4px;right:4px;min-width:16px;height:16px;padding:0 4px;
  border-radius:99px;background:var(--signal);color:#fff;
  font-size:10px;font-weight:700;line-height:16px;text-align:center;
  box-shadow:0 0 0 2.5px var(--bg);
}

/* -- hero -------------------------------------------------- */
.hero{position:relative;padding:10px 2px 0}
.hero-eyebrow{
  margin:0;font-size:11.5px;font-weight:600;
  letter-spacing:.13em;text-transform:uppercase;color:var(--dim);
}
.hero h1{
  margin:9px 0 0;font-size:35px;line-height:1.1;
  font-weight:800;letter-spacing:-.03em;text-wrap:balance;color:var(--tx);
}
.hero h1 em{font-style:normal;color:var(--signal)}

/* -- live status ribbon ------------------------------------ */
/* One line that answers "is anything wrong?". The waves are the logo's
   own NFC motif, animating so the app reads as actively listening
   rather than as a static report. */
.status{
  display:flex;align-items:center;gap:15px;margin-top:22px;
  padding:15px 16px;border-radius:var(--r-card);
  background:var(--surface);border:1px solid var(--hair);
  box-shadow:0 1px 2px rgba(16,19,23,.04);
  transition:transform .22s var(--ease),box-shadow .22s var(--ease);
}
.status:active{transform:scale(.985)}
.status.alarm{background:var(--wash);border-color:var(--edge)}

/* Concentric arcs expanding from a solid core. */
.live{
  position:relative;width:38px;height:38px;flex:0 0 38px;
  display:grid;place-items:center;
}
.live .core{
  width:9px;height:9px;border-radius:99px;background:var(--live);
  position:relative;z-index:2;
}
.status.alarm .live .core{background:var(--signal)}
.live i{
  position:absolute;inset:0;border-radius:99px;
  border:1.5px solid var(--live);
  animation:listen 2.6s var(--ease) infinite;
}
.status.alarm .live i{border-color:var(--signal)}
.live i:nth-child(1){animation-delay:0s}
.live i:nth-child(2){animation-delay:.55s}
.live i:nth-child(3){animation-delay:1.1s}
@keyframes listen{
  0%{transform:scale(.24);opacity:0}
  18%{opacity:.85}
  100%{transform:scale(1);opacity:0}
}

.status-body{flex:1;min-width:0}
.status-title{
  display:block;font-size:16px;font-weight:600;letter-spacing:-.012em;
  color:var(--tx);
}
.status.alarm .status-title{color:var(--signalText)}
.status-sub{
  display:block;margin-top:4px;font-size:13px;line-height:1.4;color:var(--low);
}
.status .chev{color:var(--dim);flex:0 0 auto}

/* The counts still exist, just not as a dashboard: they ride in the
   section header where they are context rather than the main event. */


/* -- lost alert -------------------------------------------- */
.alert{
  display:flex;align-items:center;gap:13px;
  padding:14px 15px;border-radius:var(--r-card);
  background:var(--wash);border:1px solid var(--edge);
  transition:transform .2s var(--ease);
}
.alert:active{transform:scale(.985)}
.alert-dot{
  position:relative;width:9px;height:9px;flex:0 0 9px;border-radius:99px;
  background:var(--signal);
}
.alert-dot::after{
  content:"";position:absolute;inset:-5px;border-radius:99px;
  border:1.5px solid var(--signal);animation:ping 2s var(--ease) infinite;
}
@keyframes ping{0%{transform:scale(.55);opacity:.9}70%,100%{transform:scale(1.25);opacity:0}}
.alert p{margin:0;font-size:13.5px;line-height:1.45;color:var(--signalText);flex:1}
.alert p b{color:var(--tx);font-weight:600}

/* -- tag card ---------------------------------------------- */
.tcard{
  display:flex;align-items:center;gap:14px;
  padding:13px;border-radius:var(--r-card);
  background:var(--surface);border:1px solid var(--hair);
  box-shadow:0 1px 2px rgba(16,19,23,.04);
  transition:transform .22s var(--ease),box-shadow .22s var(--ease);
}
.tcard:active{transform:scale(.982);box-shadow:0 1px 2px rgba(16,19,23,.02)}
.tcard.lost{background:var(--wash);border-color:var(--edge)}

/* The photo sits on a tinted plinth rather than straight on white, so a
   real photo and a fallback glyph carry the same visual weight. */
.tcard .shot{
  position:relative;width:62px;height:62px;flex:0 0 62px;
  border-radius:var(--r-thumb);overflow:hidden;
  background:linear-gradient(155deg,var(--alt),var(--sunk));
  display:grid;place-items:center;color:var(--dim);
}
.tcard .shot img{width:100%;height:100%;object-fit:cover}
/* Hairline INSIDE the radius - a real border fights the image edge. */
.tcard .shot::after{
  content:"";position:absolute;inset:0;border-radius:inherit;
  box-shadow:inset 0 0 0 1px rgba(16,19,23,.08);pointer-events:none;
}
.tcard.lost .shot{background:linear-gradient(155deg,rgba(255,77,23,.16),rgba(255,77,23,.07))}

.tcard .body{flex:1;min-width:0}
.tcard .nm{
  display:block;font-size:16.5px;font-weight:600;letter-spacing:-.012em;
  overflow:hidden;text-overflow:ellipsis;white-space:nowrap;
}
.tcard .meta{display:flex;align-items:center;gap:7px;margin-top:6px;flex-wrap:wrap}
.tcard .chip{
  font-size:10.5px;font-weight:600;letter-spacing:.07em;text-transform:uppercase;
  color:var(--dim);background:var(--alt);border-radius:var(--r-chip);padding:3px 7px;
}
.tcard.lost .chip{background:rgba(255,77,23,.14);color:var(--signalText)}
.tcard .code{font-size:11.5px;color:var(--low);font-variant-numeric:tabular-nums}

.tcard .end{display:flex;align-items:center;gap:9px;flex:0 0 auto;color:var(--dim)}
.tcard .scans{font-size:12px;font-variant-numeric:tabular-nums;color:var(--low)}
.lostflag{
  font-size:10px;font-weight:700;letter-spacing:.1em;
  background:var(--signal);color:#fff;border-radius:99px;padding:5px 10px;
}

/* Add a tag: an invitation, not a card pretending to hold something. */
.tadd{
  display:flex;align-items:center;justify-content:center;gap:10px;
  padding:17px;border-radius:var(--r-card);
  border:1.5px dashed var(--hairUp);color:var(--dim);font-size:14.5px;
  transition:border-color .22s var(--ease),color .22s var(--ease),transform .22s var(--ease);
}
.tadd:active{transform:scale(.985)}
.tadd:hover{border-color:var(--edge);color:var(--signalText)}
.tadd i{
  width:24px;height:24px;border-radius:99px;display:grid;place-items:center;
  background:var(--signal);color:#fff;font-style:normal;font-size:15px;
  font-weight:600;line-height:1;
}

/* -- activity feed ----------------------------------------- */
/* Icon chips give each entry a type at a glance. The old plain two-line
   list gave every event identical weight and no type at all. */
.fitem{display:flex;gap:13px;align-items:flex-start;padding:12px 0}
.fitem + .fitem{border-top:1px solid var(--hair)}
.fico{
  width:34px;height:34px;flex:0 0 34px;border-radius:11px;
  display:grid;place-items:center;
  background:var(--alt);color:var(--dim);
}
.fico.scan{background:rgba(15,110,74,.10);color:var(--live)}
.fico.lost{background:var(--wash);color:var(--signalText)}
.fbody{flex:1;min-width:0}
.ftitle{display:block;font-size:14.5px;line-height:1.35;font-weight:500;color:var(--tx)}
.fitem.unread .ftitle{font-weight:600}
.fsub{
  display:block;margin-top:3px;font-size:12.5px;color:var(--low);
  overflow:hidden;text-overflow:ellipsis;white-space:nowrap;
}
.ftime{
  flex:0 0 auto;font-size:11.5px;color:var(--low);
  font-variant-numeric:tabular-nums;padding-top:9px;
}
.fitem .unreaddot{
  width:6px;height:6px;border-radius:99px;background:var(--signal);
  flex:0 0 6px;margin-top:13px;
}

/* -- staggered entrance ------------------------------------ */
/* Driven by --i so the order survives conditional blocks. The .st
   nth-child delays break the moment a banner appears or disappears. */
.stag{animation:rise .46s var(--ease) backwards;animation-delay:calc(var(--i,0) * 55ms)}

@media (prefers-reduced-motion:reduce){
  .stag{animation:none}
  .alert-dot::after{animation:none}
  .tcard,.tadd,.bar-act,.alert{transition:none}
}

/* =====================================================================
   SCREEN TRANSITIONS
   ---------------------------------------------------------------------
   Direction is set by the router on <html>: data-nav="push" when going
   deeper, "pop" when coming back. Without that cue both directions look
   identical and the app feels like a page reload.
   ===================================================================== */

/* Screen-change animation lives at the end of this file. The View
   Transitions API is deliberately NOT used: it snapshots the ROOT,
   and the header and tab bar sit outside the content container, so
   they were inside that snapshot and flashed on every navigation. */

/* The tab bar must NOT ride the transition - a bottom bar that slides
   with the content reads as a bug, not a flourish. */
/* The bar is no longer inside the swapped view, so it needs no
   view-transition-name at all - and must not have one, because a
   backdrop-filter cannot be captured as a transparent snapshot. */
.tabs[hidden]{display:none}

.nav-anim{animation:.34s cubic-bezier(.22,1,.36,1) both;will-change:opacity,transform}
.nav-anim.push{animation-name:screenFromRight}
.nav-anim.pop{animation-name:screenFromLeft}
.nav-anim.right{animation-name:screenFromRight}
.nav-anim.left{animation-name:screenFromLeft}
.nav-anim.swap{animation-name:screenFromRight}

@keyframes screenFromRight{
  from{opacity:0;transform:translateX(15%)}
  to{opacity:1;transform:none}
}
@keyframes screenFromLeft{
  from{opacity:0;transform:translateX(-15%)}
  to{opacity:1;transform:none}
}
/* A slide inside a centred column must not create a scrollbar. */
body{overflow-x:hidden}

@media (prefers-reduced-motion:reduce){.nav-anim{animation:none}}

/* =====================================================================
   LIVE LOCATION
   ===================================================================== */
.live-wrap{position:relative;margin-top:18px}

.radar{
  position:relative;width:100%;aspect-ratio:1;max-width:340px;margin:0 auto;
  /* 50%, not a pixel value: 99px on a 340px box is a rounded SQUARE. */
  border-radius:50%;
  background:
    radial-gradient(circle at 50% 50%, rgba(255,77,23,.07), transparent 62%),
    linear-gradient(158deg,#171B21,#0E1114 70%);
  box-shadow:0 22px 50px -24px rgba(16,19,23,.6), inset 0 0 0 1px rgba(255,255,255,.06);
  overflow:hidden;
}
.radar canvas{position:absolute;inset:0;width:100%;height:100%}

/* The sweep. Ambient, not decorative noise - it is what tells you the
   screen is live rather than a frozen picture. */
.radar .sweep{
  position:absolute;inset:0;border-radius:50%;pointer-events:none;
  background:conic-gradient(from 0deg, rgba(255,77,23,.22), transparent 32%);
  animation:radarSweep 4.2s linear infinite;
}
@keyframes radarSweep{to{transform:rotate(360deg)}}
.radar.stale .sweep{animation:none;opacity:.18}

.live-head{display:flex;align-items:center;gap:11px;margin-bottom:16px}
.live-badge{
  display:inline-flex;align-items:center;gap:8px;
  padding:7px 13px;border-radius:99px;
  background:var(--liveWash);color:var(--live);
  font-size:12px;font-weight:700;letter-spacing:.08em;text-transform:uppercase;
}
.live-badge.off{background:var(--alt);color:var(--dim)}
.live-badge i{
  position:relative;width:8px;height:8px;border-radius:99px;background:currentColor;
}
.live-badge:not(.off) i::after{
  content:"";position:absolute;inset:-5px;border-radius:99px;
  border:1.5px solid currentColor;animation:ping 2s var(--ease) infinite;
}

/* The two numbers that matter, big enough to read at arm's length. */
.live-stats{display:flex;gap:14px;margin-top:22px}
.live-stat{
  flex:1;padding:16px;border-radius:var(--r-card);
  background:var(--surface);border:1px solid var(--hair);
}
.live-stat b{
  display:block;font-size:29px;font-weight:800;letter-spacing:-.03em;
  font-variant-numeric:tabular-nums;line-height:1.05;
}
.live-stat span{
  display:block;margin-top:5px;font-size:10.5px;font-weight:600;
  letter-spacing:.1em;text-transform:uppercase;color:var(--dim);
}

.live-note{
  margin-top:18px;padding:14px 15px;border-radius:var(--r-card);
  background:var(--alt);font-size:13px;line-height:1.5;color:var(--low);
}
.live-empty{text-align:center;padding:36px 0}
.live-empty .radar{opacity:.45;max-width:190px}

@media (prefers-reduced-motion:reduce){
  .radar .sweep{animation:none;opacity:.2}
}
