/* Notegirl skin base — the structural CSS the shared engine depends on.
   Every skin includes this FIRST, then overrides freely. Rules here are kept
   at low specificity (single class) so a skin can restyle anything.
   Only geometry/behaviour that the engine assumes lives here; all colour and
   personality belongs to the skin. */

:root {
    --ease: cubic-bezier(0.4, 0, 0.2, 1);
    --sat: env(safe-area-inset-top, 0px);
    --sab: env(safe-area-inset-bottom, 0px);
    --ok: #2F9E7E;
    --bad: #E8534A;
}

* { margin: 0; padding: 0; box-sizing: border-box; -webkit-tap-highlight-color: transparent; }
html, body { min-height: 100%; }
/* Keep the scrollbar's lane reserved even while the page is short. Otherwise
   the first received photo makes the page taller than the window, a 15px
   scrollbar appears (Windows, Linux, some Macs), and the centred column
   jumps 7px to the left. Overlay scrollbars (phones, most Macs) take no room,
   so this changes nothing there. */
html { scrollbar-gutter: stable; }
/* No accidental pull-to-refresh on phones: a stray downward drag at the top
   used to reload the page and end the connection. */
html, body { overscroll-behavior-y: contain; }
body { overflow-x: hidden; }
button { font: inherit; color: inherit; background: none; border: none; cursor: pointer; -webkit-appearance: none; }
img, svg { max-width: 100%; }

/* Inline icons the engine injects (icon(name) → <svg class="ic">). */
.ic { width: 1.05em; height: 1.05em; vertical-align: -0.18em; display: inline-block; flex-shrink: 0; }
.item-ic .ic { width: 22px; height: 22px; vertical-align: middle; }
.toast .ic { margin-right: 6px; }

/* Tap targets: never below 44px on touch. */
.btn, .row input, textarea { min-height: 48px; }
.sbtn { min-height: 36px; }

/* ---- Items the engine creates ---------------------------------------
   <div class="item [mine] [justsent] [done] [failed]" data-dir="out|in" data-kind="text|file">
     <div class="item-head"><span class="item-ic"/><span class="item-name"/><span class="item-meta"/></div>
     text →  <div class="item-text"/> <div class="item-btns"><button class="sbtn">Copy</button></div>
     file →  <img/>? <div class="prog"><i/></div>? <div class="item-btns"><a class="sbtn">Save</a>…</div>
   </div>                                                                */
/* A skin may size an item to its content (chat bubbles do). Without these an
   incoming 4000px photo drags the bubble past the edge of the screen. */
/* No overflow:hidden here — skins hang badges and tape over the edge on
   purpose. Oversized photos are capped on the image itself, below. */
.item { position: relative; max-width: 100%; min-width: 0; }
.item-head { display: flex; align-items: center; gap: 10px; }
.item-name { flex: 1; min-width: 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.item-meta { white-space: nowrap; font-variant-numeric: tabular-nums; }
.item-text { white-space: pre-wrap; word-break: break-word; max-height: 260px; overflow: auto; }
.item-btns { display: flex; gap: 8px; flex-wrap: wrap; }
.item img { display: block; border-radius: 6px; max-width: 100%; height: auto; max-height: min(52vh, 420px); object-fit: contain; }
.item img { cursor: zoom-in; }
/* Full-size photo viewer (engine: openLightbox). Deliberately no transition:
   it should feel like the photo was already there, because it was. */
.lightbox { position: fixed; inset: 0; z-index: 100; user-select: none; -webkit-user-select: none; background: rgba(22, 20, 18, 0.94); display: flex; align-items: center; justify-content: center; padding: calc(var(--sat) + 8px) 8px calc(var(--sab) + 8px); cursor: zoom-out; }
.lightbox .lb-stage { position: relative; visibility: hidden; }          /* shown once the engine has sized it */
.lightbox.ready .lb-stage { visibility: visible; }
.lightbox .lb-stage[data-mode="above"] { padding-top: 48px; }            /* a row above the picture for the name and the button */
.lightbox .lb-stage[data-mode="beside"] { padding: 0 48px; }             /* a column each side: name on the left, button on the right */
/* wide gutter: the name sits in the dark space left of the picture, top-aligned, right-aligned */
.lightbox .lb-stage[data-name="side"] .lb-name { right: auto; text-align: right; }
/* narrow gutter: the name reads up the picture's left edge */
.lightbox .lb-stage[data-name="spine"] .lb-name { right: auto; left: 0; width: 40px; line-height: 40px; writing-mode: vertical-rl; text-align: left; }   /* starts at the top-left corner, reads downward */
.lightbox img { display: block; -webkit-user-drag: none; max-width: none; max-height: none; object-fit: fill; border-radius: 0; box-shadow: 0 10px 40px rgba(0, 0, 0, 0.5); cursor: zoom-out; }
.lightbox .lb-close { position: absolute; top: 0; right: 0; width: 40px; height: 40px; display: flex; align-items: center; justify-content: center; border-radius: 10px; color: #fff; background: rgba(255, 255, 255, 0.16); border: 1px solid rgba(255, 255, 255, 0.45); cursor: pointer; padding: 0; }
.lightbox .lb-close:hover { background: rgba(255, 255, 255, 0.28); }
.lightbox .lb-name { position: absolute; top: 0; left: 0; right: 48px; height: 40px; line-height: 40px; color: #fff; opacity: 0.85; font: 14px/40px -apple-system, "Segoe UI", Roboto, sans-serif; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.lightbox .lb-hint { position: absolute; bottom: calc(var(--sab) + 12px); left: 0; right: 0; text-align: center; color: rgba(255, 255, 255, 0.6); font: 13px -apple-system, "Segoe UI", Roboto, sans-serif; pointer-events: none; }
.lightbox [hidden] { display: none; }
/* Safety net, deliberately more specific than a skin's own `.item img` so it
   always applies. A skin that sizes bubbles to their content makes a plain
   `max-width:100%` circular — the bubble grows to the photo, the photo fills
   the bubble — and a 4000px photo then runs off the screen. The viewport term
   cannot be circular, so this caps it whatever the skin does. */
.item[data-kind="file"] img { max-width: min(100%, calc(100vw - 2rem)); }
/* Never let a field push out of the card it sits in. */
textarea, input { max-width: 100%; }
.item.failed .item-meta { color: var(--bad); }
.item.done .prog { display: none; }

/* Transfer progress bar. */
.prog { height: 5px; border-radius: 3px; overflow: hidden; background: rgba(0, 0, 0, 0.1); }
.prog i { display: block; height: 100%; width: 0; background: currentColor; transition: width 0.15s linear; }

/* Flash on something you just sent, so it never reads as "nothing happened". */
.item.justsent { animation: justsent 1.5s var(--ease); }
@keyframes justsent {
    0%, 65% { box-shadow: 0 0 0 3px var(--ok); }
    100% { box-shadow: 0 0 0 0 rgba(0, 0, 0, 0); }
}

/* Shake the connect card when an action needs a friend first. */
.nudge { animation: nudge 0.6s var(--ease); }
@keyframes nudge {
    0%, 100% { transform: translateX(0); }
    20% { transform: translateX(-7px); }
    45% { transform: translateX(7px); }
    70% { transform: translateX(-3px); }
}

/* Connection dot: engine sets .dot plus one of wait/on/err. */
.dot { width: 10px; height: 10px; border-radius: 50%; flex-shrink: 0; background: #A39A90; }
.dot.wait { background: #FFD36E; animation: pulse 1.4s ease-in-out infinite; }
.dot.on { background: #7ED9C0; }
.dot.err { background: var(--bad); }
@keyframes pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.35; } }

/* The three code words, revealed one at a time. */
/* Words fade in only — no movement or scaling, so nothing jitters as they land.
   A skin can leave three placeholder chips (.w.skel) in #myCode so the box is
   already its final height before the words arrive; the engine replaces them. */
.code .w { display: inline-block; animation: codeIn 0.45s ease-out backwards; }
@keyframes codeIn { 0% { opacity: 0; } 100% { opacity: 1; } }
.code .w.skel { animation: none; color: transparent; background: rgba(0, 0, 0, 0.06); border-color: transparent; }

/* "Keep Notegirl handy" line the engine adds under the list after the first
   thing arrives (see showKeepNudge). Skins restyle it; this keeps it tidy. */
.keep { display: flex; flex-wrap: wrap; align-items: center; gap: 8px 12px; margin-top: 10px; }
.keep-btns { display: flex; gap: 8px; margin-left: auto; }
.keep-steps { margin: 4px 0 0; padding-left: 20px; }
.keep-key { font: inherit; font-weight: 700; padding: 0 5px; border: 1px solid currentColor; border-radius: 6px; }
.keep-ic { width: 1em; height: 1em; vertical-align: -0.15em; }

/* Toast. Engine toggles .show. */
.toast {
    position: fixed; left: 50%; bottom: calc(var(--sab) + 22px); z-index: 70;
    transform: translateX(-50%) translateY(20px); opacity: 0;
    transition: opacity 0.25s, transform 0.25s; pointer-events: none;
    max-width: min(90vw, 420px); text-align: center;
    display: flex; align-items: center; justify-content: center;
    overflow-wrap: anywhere; /* a long file name in "Received: …" wraps instead of running off the edge */
}
.toast.show { opacity: 1; transform: translateX(-50%); }

/* Pad visibility: .show once linked, .offline once the friend goes away. */
#pad { display: none; }
#pad.show { display: block; }

/* Send animation flourishes the engine appends to <body>. */
.fly { position: fixed; z-index: 60; pointer-events: none; transform: translate(-50%, -50%); animation: flyAway 0.9s cubic-bezier(0.2, 0.7, 0.3, 1) forwards; }
.fly .ic { width: 30px; height: 30px; }
@keyframes flyAway {
    0% { opacity: 1; transform: translate(-50%, -50%) rotate(-8deg) scale(1); }
    100% { opacity: 0; transform: translate(calc(-50% + 40px), calc(-50% - 150px)) rotate(14deg) scale(0.5); }
}
.spark { position: fixed; z-index: 60; width: 9px; height: 9px; border-radius: 2px; pointer-events: none; transform: translate(-50%, -50%); animation: sparkOut 0.85s ease-out forwards; }
@keyframes sparkOut { 100% { transform: translate(calc(-50% + var(--dx)), calc(-50% + var(--dy))) rotate(200deg) scale(0.15); opacity: 0; } }


/* ---- The mark, inline (site/brand-mark.html) ----------------------------
   A skin colours her with variables. --logo-face can be the page background,
   a tint of it, or the pink; --logo-hair carries hair, eyes and smile.
   She blinks now and then; while the pad is waiting for a friend she sends
   out quiet sonar rings; when the link opens one ring comes home and she
   squints happily. All transform/opacity only, and off under reduced motion. */
.brand-mark { overflow: visible; display: inline-block; flex: none; }
.brand-mark .ng-hair, .brand-mark .ng-eye, .brand-mark .ng-smile { fill: var(--logo-hair, #D73462); }
.brand-mark .ng-face { fill: var(--logo-face, #EAADBE); }
.brand-mark .ng-rings circle { fill: none; stroke: var(--logo-ring, var(--logo-hair, #D73462)); stroke-width: 3; opacity: 0; transform-box: fill-box; transform-origin: center; }
.brand-mark .ng-eye { transform-box: fill-box; transform-origin: center; animation: ng-blink 23s linear 0.6s infinite; }
@keyframes ng-blink {
    0% { transform: scaleY(1); }
    3.913% { transform: scaleY(1); }
    4.087% { transform: scaleY(0.08); }
    4.130% { transform: scaleY(0.08); }
    4.522% { transform: scaleY(1); }
    18.261% { transform: scaleY(1); }
    18.435% { transform: scaleY(0.08); }
    18.478% { transform: scaleY(0.08); }
    18.870% { transform: scaleY(1); }
    19.304% { transform: scaleY(1); }
    19.478% { transform: scaleY(0.08); }
    19.522% { transform: scaleY(0.08); }
    19.913% { transform: scaleY(1); }
    37.826% { transform: scaleY(1); }
    38.000% { transform: scaleY(0.08); }
    38.043% { transform: scaleY(0.08); }
    38.435% { transform: scaleY(1); }
    52.609% { transform: scaleY(1); }
    52.783% { transform: scaleY(0.08); }
    52.826% { transform: scaleY(0.08); }
    53.217% { transform: scaleY(1); }
    68.696% { transform: scaleY(1); }
    68.870% { transform: scaleY(0.08); }
    68.913% { transform: scaleY(0.08); }
    69.304% { transform: scaleY(1); }
    88.261% { transform: scaleY(1); }
    88.435% { transform: scaleY(0.08); }
    88.478% { transform: scaleY(0.08); }
    88.870% { transform: scaleY(1); }
    100% { transform: scaleY(1); }
}
/* listening for a friend */
body:has(#dot.wait) .brand-mark .ng-rings circle { animation: ng-sonar 2.7s cubic-bezier(0.2, 0.6, 0.3, 1) infinite; }
body:has(#dot.wait) .brand-mark .ng-rings circle:nth-child(2) { animation-delay: 0.9s; }
body:has(#dot.wait) .brand-mark .ng-rings circle:nth-child(3) { animation-delay: 1.8s; }
@keyframes ng-sonar {
    0% { transform: scale(0.96); opacity: 0.5; }
    100% { transform: scale(2.15); opacity: 0; }
}
/* the link opened */
body:has(#dot.on) .brand-mark .ng-rings circle:first-child { animation: ng-resolve 0.8s cubic-bezier(0.3, 0.7, 0.2, 1) 1 both; }
@keyframes ng-resolve {
    0% { transform: scale(2.15); opacity: 0; }
    30% { opacity: 0.6; }
    100% { transform: scale(1); opacity: 0; }
}
body:has(#dot.on) .brand-mark .ng-eye { animation: ng-happy 1.1s cubic-bezier(0.3, 0.7, 0.2, 1) 1, ng-blink 23s linear 1.2s infinite; }
@keyframes ng-happy {
    0% { transform: scaleY(1); }
    25%, 60% { transform: scaleY(0.28); }
    100% { transform: scaleY(1); }
}

@media (prefers-reduced-motion: reduce) {
    *, *::before, *::after { animation: none !important; transition: none !important; }
    /* The blink stays: a 40ms eyelid on a 48px mark is not motion in the
       vestibular sense, and Windows reports reduced motion whenever its
       "animation effects" switch is off, which would leave her frozen. The
       sonar rings and the resolve flourish, which do move, stay off. */
    .brand-mark .ng-eye { animation: ng-blink 23s linear 0.6s infinite !important; }
}
