nixamp 0.27.0 → 0.27.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/README.md +14 -4
  2. package/backtoschool/dist/assets/{index-6vtCMzi1.css → index-OE2xWZUH.css} +1 -1
  3. package/backtoschool/dist/assets/index-pQ_uH4bZ.js +93 -0
  4. package/backtoschool/dist/index.html +25 -9
  5. package/dist/accounts.d.ts +2 -0
  6. package/dist/accounts.js +9 -2
  7. package/dist/classroom.d.ts +15 -0
  8. package/dist/classroom.js +54 -0
  9. package/dist/live-events.d.ts +7 -1
  10. package/dist/live-events.js +88 -12
  11. package/dist/live-voice.d.ts +1 -1
  12. package/dist/live-voice.js +7 -7
  13. package/dist/password-reset-page.d.ts +2 -0
  14. package/dist/password-reset-page.js +106 -0
  15. package/dist/password-reset.d.ts +35 -0
  16. package/dist/password-reset.js +119 -0
  17. package/dist/server.d.ts +3 -0
  18. package/dist/server.js +96 -2
  19. package/dist/shared-translation.js +25 -17
  20. package/dist/tokens.d.ts +2 -0
  21. package/dist/tokens.js +2 -0
  22. package/dist/translation-passes.d.ts +13 -8
  23. package/dist/translation-passes.js +31 -20
  24. package/dist/upgrade-allowance.d.ts +23 -0
  25. package/dist/upgrade-allowance.js +70 -0
  26. package/package.json +1 -1
  27. package/src/accounts.ts +8 -2
  28. package/src/classroom.ts +52 -0
  29. package/src/live-events.ts +81 -13
  30. package/src/live-voice.ts +7 -7
  31. package/src/password-reset-page.ts +108 -0
  32. package/src/password-reset.ts +134 -0
  33. package/src/server.ts +77 -2
  34. package/src/shared-translation.ts +20 -14
  35. package/src/tokens.ts +3 -0
  36. package/src/translation-passes.ts +35 -25
  37. package/src/types/auth-system.d.ts +10 -0
  38. package/src/upgrade-allowance.ts +69 -0
  39. package/web/dist/assets/{hls-3VKVEQE3-P3sj2d3a.js → hls-3VKVEQE3-CAL4qBEq.js} +1 -1
  40. package/web/dist/assets/index-CLeEWlms.js +3 -0
  41. package/web/dist/assets/{index-BmR7T0zX.css → index-D34dTY18.css} +1 -1
  42. package/web/dist/assets/{mpegts-CZ1YoF1U.js → mpegts-H9oC2fAK.js} +1 -1
  43. package/web/dist/assets/{mpegts-LO6RVLD6-DuNM0Zxm.js → mpegts-LO6RVLD6-Bkd9Sqr6.js} +1 -1
  44. package/web/dist/index.html +6 -5
  45. package/web/dist/sw.js +6 -6
  46. package/backtoschool/dist/assets/index-D7Oas1m5.js +0 -88
  47. package/web/dist/assets/index-Do9VYiU8.js +0 -3
package/README.md CHANGED
@@ -557,6 +557,16 @@ uses its direct model; it does not first translate the audio into English.
557
557
 
558
558
  ### Hear it in your language
559
559
 
560
+ Every signed-in account gets **five free live-use sessions per UTC day**, shared
561
+ across Nixamp's paid panels and upgrades. There is no timed cutoff: a session
562
+ continues while its listener remains, within the existing API usage limits.
563
+ Reconnects to the same session within 90 seconds reuse it. A stopped session
564
+ expires after that grace period; starting again then uses another allowance.
565
+ The counter resets at midnight UTC without interrupting an ongoing session.
566
+ Balance polling, captions and individual voice chunks never consume new sessions.
567
+ Free usage has zero customer charge and leaves purchased credit untouched.
568
+ The panel shows the remaining allowance alongside any purchased credit.
569
+
560
570
  **Buy translated audio** (`$` in the player or Transcript title bar) offers
561
571
  prepaid, account-bound passes: **$5 / 24 hours**, **$25 / 7 days**, or **$100 /
562
572
  30 days**. Each purchase provides that many dollars of usage credit, not
@@ -587,15 +597,15 @@ multiplied by five. Credentials and balances never travel in checkout URLs.
587
597
  New checkout creation is capped at five per account and fifty per account server
588
598
  per UTC day, plus IP and request throttles; retries reuse the original invoice.
589
599
 
590
- Account servers require a paid pass by default. Configure `COINPAY_X402_KEY`
600
+ After the five free sessions, new sessions require purchased credit. Configure `COINPAY_X402_KEY`
591
601
  with `payments:create` permission and at least one business wallet; the scoped
592
- key supplies the merchant identity. Existing credit still works during a
602
+ key supplies the merchant identity. Free sessions and existing credit still work during a
593
603
  checkout outage. A self-hosted operator explicitly sponsoring API usage may set
594
604
  `NIXAMP_TRANSLATION_BILLING=off`.
595
605
 
596
606
  Live Nixamp channels share **one recognition, translation, and voice pipeline
597
- per source and target language** on the account server. Every listening account
598
- pays the same access rate; joining adds no extra recognition or voice generation.
607
+ per source and target language** on the account server. Each listening account
608
+ uses its own free allowance or credit; joining adds no extra recognition or voice generation.
599
609
  The pipeline persists while anyone remains and closes its source and pending
600
610
  work when the last listener leaves. Disconnecting one viewer does not stop the
601
611
  others. Two connections per account, four active source/language pipelines, and
@@ -1 +1 @@
1
- :root{--ink:#201a32;--purple:#34246d;--purple-2:#5b43a4;--coral:#f56f5d;--yellow:#ffd465;--paper:#f7f2e8;--paper-deep:#eee5d6;--white:#fffdf8;--green:#217a53;--muted:#6f6879;--line:#201a3224;--shadow:0 18px 50px #34246d1f;color:var(--ink);background:var(--paper);font-synthesis:none;font-family:Inter,ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,Segoe UI,sans-serif}*{box-sizing:border-box}html{scroll-behavior:smooth}body{background:var(--paper);min-width:320px;min-height:100vh;margin:0}button,input,select,textarea{font:inherit}button,a{-webkit-tap-highlight-color:transparent}a{color:inherit}[hidden]{display:none!important}body:before{content:"";pointer-events:none;opacity:.34;background-image:radial-gradient(#34246d1f .65px,#0000 .65px);background-size:7px 7px;position:fixed;inset:0;-webkit-mask-image:linear-gradient(#000,#0000 55%);mask-image:linear-gradient(#000,#0000 55%)}#app{flex-direction:column;min-height:100vh;display:flex;position:relative}.site-header,main,.site-footer{width:min(1180px,100% - 40px);margin-inline:auto}.site-header{border-bottom:1px solid var(--line);justify-content:space-between;align-items:center;gap:24px;min-height:82px;display:flex}.wordmark{color:var(--purple);letter-spacing:-.035em;align-items:center;gap:10px;font-size:1.12rem;font-weight:850;text-decoration:none;display:inline-flex}.wordmark>span:last-child span{color:var(--coral)}.wordmark-mark{width:34px;height:34px;color:var(--white);background:var(--purple);box-shadow:3px 3px 0 var(--yellow);border-radius:10px 10px 10px 3px;place-items:center;font-family:Georgia,serif;font-size:1.2rem;display:grid;transform:rotate(-3deg)}.site-nav{align-items:center;gap:28px;font-size:.91rem;display:flex}.site-nav a{color:var(--muted);text-decoration:none}.site-nav a:hover{color:var(--purple)}main{outline:none;flex:1}.loading-page,.error-page{text-align:center;place-content:center;min-height:68vh;display:grid}.loading-page h1,.error-page h1{color:var(--purple);margin:8px 0;font:700 clamp(2.4rem,8vw,5rem)/1 Georgia,serif}.loading-page p,.error-page p{color:var(--muted)}.hero{grid-template-columns:minmax(0,1.05fr) minmax(360px,.8fr);align-items:center;gap:clamp(50px,8vw,110px);min-height:610px;padding:92px 0 78px;display:grid}.eyebrow{color:var(--coral);letter-spacing:.13em;text-transform:uppercase;font-size:.75rem;font-weight:850;display:block}.hero h1{max-width:690px;color:var(--purple);letter-spacing:-.065em;margin:14px 0 24px;font:750 clamp(3.6rem,8vw,7.3rem)/.86 Georgia,Times New Roman,serif}.hero h1 em{color:var(--coral);font-weight:inherit}.hero-copy>p{max-width:590px;color:var(--muted);margin:0;font-size:clamp(1.02rem,1.5vw,1.22rem);line-height:1.7}.hero-actions{flex-wrap:wrap;gap:12px;margin-top:34px;display:flex}.hero-copy .hero-note{margin-top:15px;font-size:.82rem}.button{cursor:pointer;border:0;border-radius:999px;justify-content:center;align-items:center;gap:9px;min-height:44px;padding:0 22px;font-weight:780;text-decoration:none;transition:transform .16s,box-shadow .16s,background .16s;display:inline-flex}.button:hover:not(:disabled){transform:translateY(-2px)}.button:focus-visible,.text-button:focus-visible,a:focus-visible{outline-offset:3px;outline:3px solid #f56f5d66}.button:disabled{cursor:wait;opacity:.65}.button-primary{color:#fff;background:var(--purple);box-shadow:0 8px 22px #34246d3d}.button-primary:hover:not(:disabled){background:var(--purple-2);box-shadow:0 12px 28px #34246d47}.button-secondary{color:var(--purple);background:0 0;border:1px solid #34246d47}.button-light{color:var(--purple);background:var(--white)}.button-danger{color:#902d27;background:#ffe5df}.button-quiet{min-height:36px;color:var(--purple);background:#34246d12;padding-inline:17px}.button-quiet.signed-in{color:var(--green);background:#217a531a}.button-small{color:#fff;background:var(--purple);min-height:38px;padding-inline:16px;font-size:.84rem}.live-dot{background:var(--coral);border-radius:50%;width:8px;height:8px;box-shadow:0 0 0 4px #f56f5d2e}.hero-board{color:#fffbe9;background:#285e4d;border:10px solid #bd8654;border-radius:6px;flex-direction:column;justify-content:center;min-height:390px;padding:58px 50px 44px;display:flex;position:relative;transform:rotate(1.2deg);box-shadow:0 26px 60px #201a3233,inset 0 0 50px #0000001f}.hero-board:before{content:"";opacity:.2;background-image:linear-gradient(95deg,#0000 49%,#fff3 50%,#0000 52%),radial-gradient(#fff 0,#0000 55%);background-size:220px 100%,210px 35px;position:absolute;inset:0}.tape{z-index:2;background:#ffdd8dc2;width:86px;height:28px;position:absolute}.tape-one{top:-23px;left:40px;transform:rotate(-7deg)}.tape-two{bottom:-20px;right:28px;transform:rotate(-5deg)}.board-label{opacity:.75;font:italic 1rem Georgia,serif;position:relative}.hero-board strong{letter-spacing:-.035em;max-width:390px;margin:18px 0 15px;font:600 clamp(2rem,4vw,3.2rem)/1.05 Georgia,serif;position:relative}.hero-board p{opacity:.72;margin:0;position:relative}.board-action{color:#fff;cursor:pointer;background:0 0;border:0;border-bottom:1px solid #ffffff80;align-self:flex-start;margin-top:34px;padding-bottom:4px;font-weight:750;text-decoration:none;position:relative}.board-action span{color:var(--yellow)}.board-rule{background:var(--yellow);width:58px;height:3px;position:absolute;top:38px;right:40px;transform:rotate(-4deg)}.board-doodles{opacity:.34;gap:14px;font:2rem Georgia,serif;display:flex;position:absolute;bottom:26px;right:30px;transform:rotate(-8deg)}.event-section{padding:88px 0}.event-section-tint{position:relative}.event-section-tint:before{content:"";z-index:-1;background:var(--paper-deep);position:absolute;inset:24px calc(50% - 50vw)}.section-heading{justify-content:space-between;align-items:end;gap:20px;margin-bottom:30px;display:flex}.section-heading h2,.host-callout h2{color:var(--purple);letter-spacing:-.045em;margin:7px 0 0;font:700 clamp(2.3rem,5vw,4rem)/1 Georgia,serif}.section-count{color:var(--muted);font-size:.84rem}.event-grid{grid-template-columns:repeat(3,minmax(0,1fr));gap:18px;display:grid}.event-card{border:1px solid var(--line);background:#fffdf8bd;border-radius:20px;min-height:285px;transition:transform .18s,box-shadow .18s;box-shadow:0 4px 16px #34246d0a}.event-card:hover{box-shadow:var(--shadow);transform:translateY(-5px)rotate(-.3deg)}.event-card.is-live{border-color:#f56f5d73}.event-card>a{flex-direction:column;height:100%;padding:26px;text-decoration:none;display:flex}.event-card-top{justify-content:space-between;align-items:center;gap:12px;display:flex}.status-pill,.topic{width:-moz-fit-content;width:fit-content;min-height:25px;color:var(--purple);letter-spacing:.06em;text-transform:uppercase;background:#34246d14;border-radius:999px;align-items:center;padding:3px 10px;font-size:.69rem;font-weight:850;display:inline-flex}.status-pill.status-live{color:#a42d25;background:#ffe1db}.status-pill.status-live:before{content:"";background:var(--coral);border-radius:50%;width:6px;height:6px;margin-right:6px;animation:1.8s infinite pulse}.topic{color:var(--green);background:#217a5317}.topic-large{margin-top:18px}.event-card h3{color:var(--ink);letter-spacing:-.025em;margin:28px 0 11px;font:700 1.55rem/1.14 Georgia,serif}.event-card p{color:var(--muted);-webkit-line-clamp:3;-webkit-box-orient:vertical;margin:0;line-height:1.55;display:-webkit-box;overflow:hidden}.event-link{color:var(--purple);justify-content:space-between;margin-top:auto;padding-top:24px;font-size:.86rem;font-weight:800;display:flex}.empty-card{text-align:center;border:1px dashed #34246d40;border-radius:20px;grid-column:1/-1;padding:55px 30px}.empty-card>span{font-size:2rem}.empty-card h3{margin:12px 0 6px;font:700 1.5rem Georgia,serif}.empty-card p,.empty-line{color:var(--muted)}.upcoming-list{border-top:1px solid var(--line)}.upcoming-row{border-bottom:1px solid var(--line);grid-template-columns:minmax(130px,.34fr) 1fr 34px;align-items:center;gap:25px;min-height:92px;padding:16px 8px;text-decoration:none;display:grid}.upcoming-row:hover strong,.upcoming-row:hover .row-arrow{color:var(--coral)}.upcoming-row time{color:var(--purple);font-size:.83rem;font-weight:780}.upcoming-row>span:nth-child(2){flex-direction:column;gap:6px;display:flex}.upcoming-row strong{font:650 1.22rem Georgia,serif;transition:color .16s}.upcoming-row small{color:var(--muted)}.row-arrow{color:var(--purple);font-size:1.3rem}.host-callout{color:#fff;background:var(--purple);border-radius:28px;margin:90px 0 100px;padding:clamp(42px,7vw,78px);position:relative;overflow:hidden}.host-callout:after{content:"?";color:#ffffff0f;font:500 25rem Georgia,serif;position:absolute;top:-32%;right:5%;transform:rotate(12deg)}.host-callout .eyebrow{color:var(--yellow)}.host-callout h2{z-index:1;color:#fff;position:relative}.host-callout p{z-index:1;opacity:.75;max-width:520px;line-height:1.6;position:relative}.host-callout .button{z-index:1;margin-top:16px;position:relative}.site-footer{min-height:100px;color:var(--muted);border-top:1px solid var(--line);justify-content:space-between;align-items:center;font-size:.82rem;display:flex}.site-footer a{color:var(--purple);font-weight:750}.dialog{width:min(480px,100% - 28px);color:var(--ink);background:var(--white);border:0;border-radius:24px;padding:38px;box-shadow:0 24px 90px #201a324d}.dialog::backdrop{-webkit-backdrop-filter:blur(5px);backdrop-filter:blur(5px);background:#201a3294}.dialog h2{color:var(--purple);margin:7px 0 10px;font:700 2.2rem Georgia,serif}.dialog>p{color:var(--muted);margin-top:0;line-height:1.5}.dialog-wide{width:min(650px,100% - 28px)}.dialog-close{width:35px;height:35px;color:var(--muted);cursor:pointer;background:#34246d12;border:0;border-radius:50%;font-size:1.4rem;position:absolute;top:14px;right:18px}.form-stack{gap:17px;margin-top:28px;display:grid}.form-stack.compact{gap:12px;margin-top:0}.form-stack label{color:var(--ink);gap:7px;font-size:.76rem;font-weight:790;display:grid}.form-stack input,.form-stack select,.form-stack textarea,.inline-form input,.share-box input{border:1px solid var(--line);width:100%;min-height:46px;color:var(--ink);background:#fff;border-radius:11px;outline:none;padding:10px 13px}.form-stack textarea{resize:vertical}.form-stack input:focus,.form-stack select:focus,.form-stack textarea:focus,.inline-form input:focus{border-color:var(--purple-2);box-shadow:0 0 0 3px #5b43a41f}.form-stack>.button{width:100%;margin-top:4px}.form-stack.compact>.button{width:-moz-fit-content;width:fit-content}.form-note,.form-error{min-height:1.2em;color:var(--muted);overflow-wrap:anywhere;margin:0;font-size:.76rem}.form-error{color:#a42d25}.form-note:empty,.form-error:empty{display:none}.form-checks{color:var(--muted);flex-wrap:wrap;gap:8px 18px;padding:3px 0;font-size:.8rem;display:flex}.form-stack .form-checks label{cursor:pointer;align-items:center;gap:7px;font-weight:600;display:flex}.form-stack .form-checks input{width:16px;height:16px;min-height:0;accent-color:var(--purple);margin:0;padding:0}.form-stack .form-checks label:has(input:checked){color:var(--purple)}.form-pair{grid-template-columns:1fr 1fr;gap:14px;display:grid}.text-button{width:-moz-fit-content;width:fit-content;color:var(--purple);cursor:pointer;background:0 0;border:0;border-bottom:1px solid #0000;padding:0;font-weight:750}.text-button:hover{border-bottom-color:currentColor}.event-shell{padding:42px 0 100px}.back-link{color:var(--muted);margin-bottom:28px;font-size:.84rem;font-weight:700;text-decoration:none;display:inline-block}.back-link:hover{color:var(--purple)}.event-layout{grid-template-columns:minmax(0,1.55fr) minmax(280px,.75fr);align-items:start;gap:18px;display:grid}.region{gap:18px;min-width:0;display:grid}.region-sidebar,.region-bottom,.region-drawer{grid-column:1/-1;grid-template-columns:repeat(auto-fit,minmax(270px,1fr))}.event-panel{border:1px solid var(--line);background:#fffdf8d1;border-radius:20px;min-width:0;padding:28px;position:relative;box-shadow:0 4px 20px #34246d0a}.panel-title{color:var(--muted);letter-spacing:.1em;text-transform:uppercase;margin-bottom:18px;font-size:.68rem;font-weight:850;display:block}.panel-event-header{color:#fff;background:var(--purple);border:0;align-items:end;min-height:320px;padding:clamp(34px,6vw,62px);display:flex;overflow:hidden}.panel-event-header:after{content:"✦";color:#ffffff12;font-size:15rem;position:absolute;top:-80px;right:-20px;transform:rotate(18deg)}.panel-event-header .panel-title{display:none}.event-heading{z-index:1;position:relative}.event-heading .topic{color:#225b42;background:#cbe9ce;margin-left:8px}.event-heading h1{letter-spacing:-.055em;max-width:780px;margin:24px 0 15px;font:700 clamp(2.7rem,7vw,5.4rem)/.95 Georgia,serif}.event-heading p{color:#ffffffad;max-width:680px;margin:0;font-size:1.03rem;line-height:1.6}.listen-card{grid-template-columns:150px 1fr;align-items:center;gap:30px;min-height:245px;display:grid}.listen-card h2,.stage-card h2{color:var(--purple);margin:7px 0 10px;font:700 2.2rem Georgia,serif}.listen-card p,.stage-card p{color:var(--muted);margin:0 0 20px;line-height:1.55}.listen-card audio{width:100%;margin-top:18px;display:none}.listen-card audio:not([src=""]){display:block}.sound-orbit{background:#ffe3dc;border-radius:50%;place-items:center;width:135px;height:135px;display:grid;position:relative}.sound-orbit b{z-index:2;color:#fff;background:var(--coral);border-radius:50%;place-items:center;width:60px;height:60px;padding-left:4px;font-size:1.25rem;display:grid}.sound-orbit span{border:1px solid #f56f5d59;border-radius:50%;animation:2.6s ease-out infinite sound;position:absolute;inset:18px}.sound-orbit span:nth-child(2){animation-delay:.8s}.sound-orbit span:nth-child(3){animation-delay:1.6s}.waiting .sound-orbit span{animation-play-state:paused}.host-line,.join-line,.people-row,.control-line{align-items:center;gap:14px;display:flex}.host-line small,.people-row small{color:var(--muted);display:block}.host-line strong,.people-row strong{margin-top:2px;display:block}.avatar,.stage-avatar{color:#fff;background:var(--purple);border-radius:14px 14px 14px 4px;flex:none;place-items:center;width:47px;height:47px;font:700 1.2rem Georgia,serif;display:grid}.avatar-small{border-radius:10px 10px 10px 3px;width:32px;height:32px;font-size:.82rem}.reading-copy{color:var(--muted);margin:0;font:1.05rem/1.7 Georgia,serif}.join-line{justify-content:space-between}.join-line strong{color:var(--purple)}.join-line p{color:var(--muted);margin:4px 0 0;font-size:.82rem}.panel-empty,.muted{color:var(--muted);line-height:1.55}.metric{flex-direction:column;gap:5px;display:flex}.metric strong{color:var(--purple);font:700 1.65rem Georgia,serif}.metric span{color:var(--muted);font-size:.78rem}.stage-card{align-items:center;gap:22px;display:flex}.stage-avatar{width:88px;height:88px;color:var(--purple);background:var(--yellow);border-radius:24px 24px 24px 7px;font-size:2.2rem}.control-stack{flex-wrap:wrap;gap:10px;display:flex}.control-line{justify-content:space-between}.status-dot{background:#b9b2bd;border-radius:50%;width:10px;height:10px}.status-dot.on{background:var(--coral);box-shadow:0 0 0 5px #f56f5d26}.share-box,.inline-form{gap:8px;display:flex}.share-box input{min-width:0;color:var(--muted);background:var(--paper)}.inline-form{margin-top:15px}.inline-form input{min-width:0}.chat-messages{gap:13px;min-height:110px;max-height:300px;display:grid;overflow-y:auto}.chat-message{align-items:start;gap:10px;display:flex}.chat-message p{gap:3px;margin:0;display:grid}.chat-message strong{font-size:.76rem}.chat-message span{color:var(--muted);line-height:1.45}.hand-raises{gap:10px;display:grid}.raise-row{border-bottom:1px solid var(--line);justify-content:space-between;align-items:center;gap:12px;padding-bottom:10px;display:flex}.raise-row>span{gap:2px;display:grid}.raise-row small{color:var(--muted);text-transform:capitalize}.raise-row>div{gap:10px;display:flex}.panel-failed{border-color:#f56f5d66}@keyframes pulse{50%{opacity:.42;transform:scale(.8)}}@keyframes sound{0%{opacity:.8;transform:scale(.72)}to{opacity:0;transform:scale(1.45)}}@media (prefers-reduced-motion:reduce){*,:before,:after{scroll-behavior:auto!important;transition-duration:.01ms!important;animation-duration:.01ms!important;animation-iteration-count:1!important}}@media (max-width:900px){.hero{grid-template-columns:1fr;padding-top:70px}.hero-board{width:min(560px,92%);margin-inline:auto}.event-grid{grid-template-columns:repeat(2,minmax(0,1fr))}.event-layout{grid-template-columns:1fr}.region-secondary{grid-template-columns:repeat(auto-fit,minmax(260px,1fr))}}@media (max-width:640px){.site-header,main,.site-footer{width:min(100% - 24px,1180px)}.site-header{min-height:70px}.site-nav a{display:none}.wordmark{font-size:1rem}.hero{gap:55px;min-height:auto;padding:60px 0}.hero h1{font-size:clamp(3.5rem,20vw,5.5rem)}.hero-actions{display:grid}.hero-actions .button{width:100%}.hero-board{border-width:8px;width:96%;min-height:340px;padding:48px 29px 38px}.event-section{padding:64px 0}.event-grid{grid-template-columns:1fr}.upcoming-row{grid-template-columns:1fr 26px;gap:9px 14px;padding-block:20px}.upcoming-row time,.upcoming-row>span:nth-child(2){grid-column:1}.upcoming-row .row-arrow{grid-area:1/2/3}.host-callout{margin:64px 0;padding:42px 28px}.site-footer{flex-direction:column;justify-content:center;align-items:start;gap:6px;min-height:90px}.dialog{padding:34px 22px 24px}.form-pair{grid-template-columns:1fr}.event-shell{padding-top:28px}.event-panel{border-radius:16px;padding:22px}.panel-event-header{min-height:390px;padding:32px 23px}.event-heading h1{font-size:clamp(2.65rem,15vw,4.2rem)}.listen-card{grid-template-columns:1fr}.sound-orbit{width:108px;height:108px}.sound-orbit b{width:52px;height:52px}.join-line,.share-box,.inline-form{flex-direction:column;align-items:stretch}.join-line .button,.share-box .button,.inline-form .button{width:100%}.region-secondary,.region-sidebar,.region-bottom,.region-drawer{grid-template-columns:1fr}}
1
+ :root{--ink:#201a32;--purple:#34246d;--purple-2:#5b43a4;--coral:#f56f5d;--yellow:#ffd465;--paper:#f7f2e8;--paper-deep:#eee5d6;--white:#fffdf8;--green:#217a53;--muted:#6f6879;--line:#201a3224;--shadow:0 18px 50px #34246d1f;color:var(--ink);background:var(--paper);font-synthesis:none;font-family:Inter,ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,Segoe UI,sans-serif}*{box-sizing:border-box}html{scroll-behavior:smooth}body{background:var(--paper);min-width:320px;min-height:100vh;margin:0}button,input,select,textarea{font:inherit}button,a{-webkit-tap-highlight-color:transparent}a{color:inherit}[hidden]{display:none!important}body:before{content:"";pointer-events:none;opacity:.34;background-image:radial-gradient(#34246d1f .65px,#0000 .65px);background-size:7px 7px;position:fixed;inset:0;-webkit-mask-image:linear-gradient(#000,#0000 55%);mask-image:linear-gradient(#000,#0000 55%)}#app{flex-direction:column;min-height:100vh;display:flex;position:relative}.site-header,main,.site-footer{width:min(1180px,100% - 40px);margin-inline:auto}.site-header{border-bottom:1px solid var(--line);justify-content:space-between;align-items:center;gap:24px;min-height:82px;display:flex}.wordmark{color:var(--purple);letter-spacing:-.035em;align-items:center;gap:10px;font-size:1.12rem;font-weight:850;text-decoration:none;display:inline-flex}.wordmark>span:last-child span{color:var(--coral)}.wordmark-mark{width:34px;height:34px;color:var(--white);background:var(--purple);box-shadow:3px 3px 0 var(--yellow);border-radius:10px 10px 10px 3px;place-items:center;font-family:Georgia,serif;font-size:1.2rem;display:grid;transform:rotate(-3deg)}.site-nav{align-items:center;gap:28px;font-size:.91rem;display:flex}.site-nav a{color:var(--muted);text-decoration:none}.site-nav a:hover{color:var(--purple)}main{outline:none;flex:1}.loading-page,.error-page{text-align:center;place-content:center;min-height:68vh;display:grid}.loading-page h1,.error-page h1{color:var(--purple);margin:8px 0;font:700 clamp(2.4rem,8vw,5rem)/1 Georgia,serif}.loading-page p,.error-page p{color:var(--muted)}.hero{grid-template-columns:minmax(0,1.05fr) minmax(360px,.8fr);align-items:center;gap:clamp(50px,8vw,110px);min-height:610px;padding:92px 0 78px;display:grid}.eyebrow{color:var(--coral);letter-spacing:.13em;text-transform:uppercase;font-size:.75rem;font-weight:850;display:block}.hero h1{max-width:690px;color:var(--purple);letter-spacing:-.065em;margin:14px 0 24px;font:750 clamp(3.6rem,8vw,7.3rem)/.86 Georgia,Times New Roman,serif}.hero h1 em{color:var(--coral);font-weight:inherit}.hero-copy>p{max-width:590px;color:var(--muted);margin:0;font-size:clamp(1.02rem,1.5vw,1.22rem);line-height:1.7}.hero-actions{flex-wrap:wrap;gap:12px;margin-top:34px;display:flex}.hero-copy .hero-note{margin-top:15px;font-size:.82rem}.button{cursor:pointer;border:0;border-radius:999px;justify-content:center;align-items:center;gap:9px;min-height:44px;padding:0 22px;font-weight:780;text-decoration:none;transition:transform .16s,box-shadow .16s,background .16s;display:inline-flex}.button:hover:not(:disabled){transform:translateY(-2px)}.button:focus-visible,.text-button:focus-visible,a:focus-visible{outline-offset:3px;outline:3px solid #f56f5d66}.button:disabled{cursor:wait;opacity:.65}.button-primary{color:#fff;background:var(--purple);box-shadow:0 8px 22px #34246d3d}.button-primary:hover:not(:disabled){background:var(--purple-2);box-shadow:0 12px 28px #34246d47}.button-secondary{color:var(--purple);background:0 0;border:1px solid #34246d47}.button-light{color:var(--purple);background:var(--white)}.button-danger{color:#902d27;background:#ffe5df}.button-quiet{min-height:36px;color:var(--purple);background:#34246d12;padding-inline:17px}.button-quiet.signed-in{color:var(--green);background:#217a531a}.button-small{color:#fff;background:var(--purple);min-height:38px;padding-inline:16px;font-size:.84rem}.live-dot{background:var(--coral);border-radius:50%;width:8px;height:8px;box-shadow:0 0 0 4px #f56f5d2e}.hero-board{color:#fffbe9;background:#285e4d;border:10px solid #bd8654;border-radius:6px;flex-direction:column;justify-content:center;min-height:390px;padding:58px 50px 44px;display:flex;position:relative;transform:rotate(1.2deg);box-shadow:0 26px 60px #201a3233,inset 0 0 50px #0000001f}.hero-board:before{content:"";opacity:.2;background-image:linear-gradient(95deg,#0000 49%,#fff3 50%,#0000 52%),radial-gradient(#fff 0,#0000 55%);background-size:220px 100%,210px 35px;position:absolute;inset:0}.tape{z-index:2;background:#ffdd8dc2;width:86px;height:28px;position:absolute}.tape-one{top:-23px;left:40px;transform:rotate(-7deg)}.tape-two{bottom:-20px;right:28px;transform:rotate(-5deg)}.board-label{opacity:.75;font:italic 1rem Georgia,serif;position:relative}.hero-board strong{letter-spacing:-.035em;max-width:390px;margin:18px 0 15px;font:600 clamp(2rem,4vw,3.2rem)/1.05 Georgia,serif;position:relative}.hero-board p{opacity:.72;margin:0;position:relative}.board-action{color:#fff;cursor:pointer;background:0 0;border:0;border-bottom:1px solid #ffffff80;align-self:flex-start;margin-top:34px;padding-bottom:4px;font-weight:750;text-decoration:none;position:relative}.board-action span{color:var(--yellow)}.board-rule{background:var(--yellow);width:58px;height:3px;position:absolute;top:38px;right:40px;transform:rotate(-4deg)}.board-doodles{opacity:.34;gap:14px;font:2rem Georgia,serif;display:flex;position:absolute;bottom:26px;right:30px;transform:rotate(-8deg)}.event-section{padding:88px 0}.event-section-tint{position:relative}.event-section-tint:before{content:"";z-index:-1;background:var(--paper-deep);position:absolute;inset:24px calc(50% - 50vw)}.section-heading{justify-content:space-between;align-items:end;gap:20px;margin-bottom:30px;display:flex}.section-heading h2,.host-callout h2{color:var(--purple);letter-spacing:-.045em;margin:7px 0 0;font:700 clamp(2.3rem,5vw,4rem)/1 Georgia,serif}.section-count{color:var(--muted);font-size:.84rem}.event-grid{grid-template-columns:repeat(3,minmax(0,1fr));gap:18px;display:grid}.event-card{border:1px solid var(--line);background:#fffdf8bd;border-radius:20px;min-height:285px;transition:transform .18s,box-shadow .18s;box-shadow:0 4px 16px #34246d0a}.event-card:hover{box-shadow:var(--shadow);transform:translateY(-5px)rotate(-.3deg)}.event-card.is-live{border-color:#f56f5d73}.event-card>a{flex-direction:column;height:100%;padding:26px;text-decoration:none;display:flex}.event-card-top{justify-content:space-between;align-items:center;gap:12px;display:flex}.status-pill,.topic{width:-moz-fit-content;width:fit-content;min-height:25px;color:var(--purple);letter-spacing:.06em;text-transform:uppercase;background:#34246d14;border-radius:999px;align-items:center;padding:3px 10px;font-size:.69rem;font-weight:850;display:inline-flex}.status-pill.status-live{color:#a42d25;background:#ffe1db}.status-pill.status-live:before{content:"";background:var(--coral);border-radius:50%;width:6px;height:6px;margin-right:6px;animation:1.8s infinite pulse}.topic{color:var(--green);background:#217a5317}.topic-large{margin-top:18px}.event-card h3{color:var(--ink);letter-spacing:-.025em;margin:28px 0 11px;font:700 1.55rem/1.14 Georgia,serif}.event-card p{color:var(--muted);-webkit-line-clamp:3;-webkit-box-orient:vertical;margin:0;line-height:1.55;display:-webkit-box;overflow:hidden}.event-link{color:var(--purple);justify-content:space-between;margin-top:auto;padding-top:24px;font-size:.86rem;font-weight:800;display:flex}.empty-card{text-align:center;border:1px dashed #34246d40;border-radius:20px;grid-column:1/-1;padding:55px 30px}.empty-card>span{font-size:2rem}.empty-card h3{margin:12px 0 6px;font:700 1.5rem Georgia,serif}.empty-card p,.empty-line{color:var(--muted)}.upcoming-list{border-top:1px solid var(--line)}.upcoming-row{border-bottom:1px solid var(--line);grid-template-columns:minmax(130px,.34fr) 1fr 34px;align-items:center;gap:25px;min-height:92px;padding:16px 8px;text-decoration:none;display:grid}.upcoming-row:hover strong,.upcoming-row:hover .row-arrow{color:var(--coral)}.upcoming-row time{color:var(--purple);font-size:.83rem;font-weight:780}.upcoming-row>span:nth-child(2){flex-direction:column;gap:6px;display:flex}.upcoming-row strong{font:650 1.22rem Georgia,serif;transition:color .16s}.upcoming-row small{color:var(--muted)}.row-arrow{color:var(--purple);font-size:1.3rem}.host-callout{color:#fff;background:var(--purple);border-radius:28px;margin:90px 0 100px;padding:clamp(42px,7vw,78px);position:relative;overflow:hidden}.host-callout:after{content:"?";color:#ffffff0f;font:500 25rem Georgia,serif;position:absolute;top:-32%;right:5%;transform:rotate(12deg)}.host-callout .eyebrow{color:var(--yellow)}.host-callout h2{z-index:1;color:#fff;position:relative}.host-callout p{z-index:1;opacity:.75;max-width:520px;line-height:1.6;position:relative}.host-callout .button{z-index:1;margin-top:16px;position:relative}.site-footer{min-height:100px;color:var(--muted);border-top:1px solid var(--line);justify-content:space-between;align-items:center;font-size:.82rem;display:flex}.site-footer a{color:var(--purple);font-weight:750}.dialog{width:min(480px,100% - 28px);color:var(--ink);background:var(--white);border:0;border-radius:24px;padding:38px;box-shadow:0 24px 90px #201a324d}.dialog::backdrop{-webkit-backdrop-filter:blur(5px);backdrop-filter:blur(5px);background:#201a3294}.dialog h2{color:var(--purple);margin:7px 0 10px;font:700 2.2rem Georgia,serif}.dialog>p{color:var(--muted);margin-top:0;line-height:1.5}.dialog-wide{width:min(650px,100% - 28px)}.dialog-close{width:35px;height:35px;color:var(--muted);cursor:pointer;background:#34246d12;border:0;border-radius:50%;font-size:1.4rem;position:absolute;top:14px;right:18px}.form-stack{gap:17px;margin-top:28px;display:grid}.form-stack.compact{gap:12px;margin-top:0}.form-stack label{color:var(--ink);gap:7px;font-size:.76rem;font-weight:790;display:grid}.form-stack input,.form-stack select,.form-stack textarea,.inline-form input,.share-box input{border:1px solid var(--line);width:100%;min-height:46px;color:var(--ink);background:#fff;border-radius:11px;outline:none;padding:10px 13px}.form-stack textarea{resize:vertical}.form-stack input:focus,.form-stack select:focus,.form-stack textarea:focus,.inline-form input:focus{border-color:var(--purple-2);box-shadow:0 0 0 3px #5b43a41f}.form-stack>.button{width:100%;margin-top:4px}.form-stack.compact>.button{width:-moz-fit-content;width:fit-content}.form-note,.form-error{min-height:1.2em;color:var(--muted);overflow-wrap:anywhere;margin:0;font-size:.76rem}.form-error{color:#a42d25}.form-note:empty,.form-error:empty{display:none}.form-checks{color:var(--muted);flex-wrap:wrap;gap:8px 18px;padding:3px 0;font-size:.8rem;display:flex}.form-stack .form-checks label{cursor:pointer;align-items:center;gap:7px;font-weight:600;display:flex}.form-stack .form-checks input{width:16px;height:16px;min-height:0;accent-color:var(--purple);margin:0;padding:0}.form-stack .form-checks label:has(input:checked){color:var(--purple)}.form-pair{grid-template-columns:1fr 1fr;gap:14px;display:grid}.text-button{width:-moz-fit-content;width:fit-content;color:var(--purple);cursor:pointer;background:0 0;border:0;border-bottom:1px solid #0000;padding:0;font-weight:750}.text-button:hover{border-bottom-color:currentColor}.event-shell{padding:42px 0 100px}.back-link{color:var(--muted);margin-bottom:28px;font-size:.84rem;font-weight:700;text-decoration:none;display:inline-block}.back-link:hover{color:var(--purple)}.event-layout{grid-template-columns:minmax(0,1.55fr) minmax(280px,.75fr);align-items:start;gap:18px;display:grid}.region{gap:18px;min-width:0;display:grid}.region-sidebar,.region-bottom,.region-drawer{grid-column:1/-1;grid-template-columns:repeat(auto-fit,minmax(270px,1fr))}.event-panel{border:1px solid var(--line);background:#fffdf8d1;border-radius:20px;min-width:0;padding:28px;position:relative;box-shadow:0 4px 20px #34246d0a}.panel-title{color:var(--muted);letter-spacing:.1em;text-transform:uppercase;margin-bottom:18px;font-size:.68rem;font-weight:850;display:block}.panel-event-header{color:#fff;background:var(--purple);border:0;align-items:end;min-height:320px;padding:clamp(34px,6vw,62px);display:flex;overflow:hidden}.panel-event-header:after{content:"✦";color:#ffffff12;font-size:15rem;position:absolute;top:-80px;right:-20px;transform:rotate(18deg)}.panel-event-header .panel-title{display:none}.event-heading{z-index:1;position:relative}.event-heading .topic{color:#225b42;background:#cbe9ce;margin-left:8px}.event-heading h1{letter-spacing:-.055em;max-width:780px;margin:24px 0 15px;font:700 clamp(2.7rem,7vw,5.4rem)/.95 Georgia,serif}.event-heading p{color:#ffffffad;max-width:680px;margin:0;font-size:1.03rem;line-height:1.6}.listen-card{grid-template-columns:150px 1fr;align-items:center;gap:30px;min-height:245px;display:grid}.listen-card h2,.stage-card h2{color:var(--purple);margin:7px 0 10px;font:700 2.2rem Georgia,serif}.listen-card p,.stage-card p{color:var(--muted);margin:0 0 20px;line-height:1.55}.listen-card audio{width:100%;margin-top:18px;display:none}.listen-card audio:not([src=""]){display:block}.sound-orbit{background:#ffe3dc;border-radius:50%;place-items:center;width:135px;height:135px;display:grid;position:relative}.sound-orbit b{z-index:2;color:#fff;background:var(--coral);border-radius:50%;place-items:center;width:60px;height:60px;padding-left:4px;font-size:1.25rem;display:grid}.sound-orbit span{border:1px solid #f56f5d59;border-radius:50%;animation:2.6s ease-out infinite sound;position:absolute;inset:18px}.sound-orbit span:nth-child(2){animation-delay:.8s}.sound-orbit span:nth-child(3){animation-delay:1.6s}.waiting .sound-orbit span{animation-play-state:paused}.host-line,.join-line,.people-row,.control-line{align-items:center;gap:14px;display:flex}.host-line small,.people-row small{color:var(--muted);display:block}.host-line strong,.people-row strong{margin-top:2px;display:block}.avatar,.stage-avatar{color:#fff;background:var(--purple);border-radius:14px 14px 14px 4px;flex:none;place-items:center;width:47px;height:47px;font:700 1.2rem Georgia,serif;display:grid}.avatar-small{border-radius:10px 10px 10px 3px;width:32px;height:32px;font-size:.82rem}.reading-copy{color:var(--muted);margin:0;font:1.05rem/1.7 Georgia,serif}.join-line{justify-content:space-between}.join-line strong{color:var(--purple)}.join-line p{color:var(--muted);margin:4px 0 0;font-size:.82rem}.panel-empty,.muted{color:var(--muted);line-height:1.55}.metric{flex-direction:column;gap:5px;display:flex}.metric strong{color:var(--purple);font:700 1.65rem Georgia,serif}.metric span{color:var(--muted);font-size:.78rem}.stage-card{align-items:center;gap:22px;display:flex}.stage-avatar{width:88px;height:88px;color:var(--purple);background:var(--yellow);border-radius:24px 24px 24px 7px;font-size:2.2rem}.control-stack{flex-wrap:wrap;gap:10px;display:flex}.control-line{justify-content:space-between}.status-dot{background:#b9b2bd;border-radius:50%;width:10px;height:10px}.status-dot.on{background:var(--coral);box-shadow:0 0 0 5px #f56f5d26}.share-box,.inline-form{gap:8px;display:flex}.share-box input{min-width:0;color:var(--muted);background:var(--paper)}.inline-form{margin-top:15px}.inline-form input{min-width:0}.chat-messages{gap:13px;min-height:110px;max-height:300px;display:grid;overflow-y:auto}.chat-message{align-items:start;gap:10px;display:flex}.chat-message p{gap:3px;margin:0;display:grid}.chat-message strong{font-size:.76rem}.chat-message span{color:var(--muted);line-height:1.45}.hand-raises{gap:10px;display:grid}.raise-row{border-bottom:1px solid var(--line);justify-content:space-between;align-items:center;gap:12px;padding-bottom:10px;display:flex}.raise-row>span{gap:2px;display:grid}.raise-row small{color:var(--muted);text-transform:capitalize}.raise-row>div{gap:10px;display:flex}.panel-failed{border-color:#f56f5d66}@keyframes pulse{50%{opacity:.42;transform:scale(.8)}}@keyframes sound{0%{opacity:.8;transform:scale(.72)}to{opacity:0;transform:scale(1.45)}}@media (prefers-reduced-motion:reduce){*,:before,:after{scroll-behavior:auto!important;transition-duration:.01ms!important;animation-duration:.01ms!important;animation-iteration-count:1!important}}@media (max-width:900px){.hero{grid-template-columns:1fr;padding-top:70px}.hero-board{width:min(560px,92%);margin-inline:auto}.event-grid{grid-template-columns:repeat(2,minmax(0,1fr))}.event-layout{grid-template-columns:1fr}.region-secondary{grid-template-columns:repeat(auto-fit,minmax(260px,1fr))}}@media (max-width:640px){.site-header,main,.site-footer{width:min(100% - 24px,1180px)}.site-header{min-height:70px}.site-nav a{display:none}.wordmark{font-size:1rem}.hero{gap:55px;min-height:auto;padding:60px 0}.hero h1{font-size:clamp(3.5rem,20vw,5.5rem)}.hero-actions{display:grid}.hero-actions .button{width:100%}.hero-board{border-width:8px;width:96%;min-height:340px;padding:48px 29px 38px}.event-section{padding:64px 0}.event-grid{grid-template-columns:1fr}.upcoming-row{grid-template-columns:1fr 26px;gap:9px 14px;padding-block:20px}.upcoming-row time,.upcoming-row>span:nth-child(2){grid-column:1}.upcoming-row .row-arrow{grid-area:1/2/3}.host-callout{margin:64px 0;padding:42px 28px}.site-footer{flex-direction:column;justify-content:center;align-items:start;gap:6px;min-height:90px}.dialog{padding:34px 22px 24px}.form-pair{grid-template-columns:1fr}.event-shell{padding-top:28px}.event-panel{border-radius:16px;padding:22px}.panel-event-header{min-height:390px;padding:32px 23px}.event-heading h1{font-size:clamp(2.65rem,15vw,4.2rem)}.listen-card{grid-template-columns:1fr}.sound-orbit{width:108px;height:108px}.sound-orbit b{width:52px;height:52px}.join-line,.share-box,.inline-form{flex-direction:column;align-items:stretch}.join-line .button,.share-box .button,.inline-form .button{width:100%}.region-secondary,.region-sidebar,.region-bottom,.region-drawer{grid-template-columns:1fr}}.broadcast-options{border:1px solid var(--line);border-radius:12px;min-width:0;margin:0;padding:16px}.broadcast-options legend{padding-inline:6px;font-weight:700}.broadcast-options p{color:var(--muted);font-size:.9rem;line-height:1.5}.broadcast-options label+label{margin-top:14px}.broadcast-links{flex-wrap:wrap;gap:10px;margin-bottom:16px;display:flex}.broadcast-links .button{padding:12px;font-size:.8rem}.classroom-video{aspect-ratio:16/11;background:#101015;border-radius:12px;overflow:hidden}.classroom-video iframe{border:0;width:100%;height:100%}.broadcast-caption{margin-bottom:0}.host-avatar{object-fit:cover}.host-line a{margin-top:5px;font-size:.85rem;display:block}button[aria-disabled=true]{opacity:.65;cursor:default}.event-panel h2.panel-title{font-size:.7rem}@media (max-width:640px){.classroom-video{aspect-ratio:3/4}}@media (forced-colors:active){.button,.event-panel,.broadcast-options{border:1px solid canvastext}}.event-toolbar{justify-content:space-between;align-items:center;gap:16px;margin-bottom:24px;display:flex}.event-toolbar .back-link{margin:0}.site-nav{min-width:0}.wordmark{flex-shrink:0}#account-button{text-overflow:ellipsis;white-space:nowrap;max-width:100%;overflow:hidden}.event-heading .status-pill{color:var(--purple);background:#eee8ff}.event-heading .status-pill.status-live{color:#a42d25;background:#ffe1db}@media (max-width:640px){.site-header{gap:12px}}.class-update{z-index:20;border:1px solid var(--line);background:var(--cream,#f7f3e9);border-radius:12px;width:min(430px,100% - 24px);padding:14px 18px;position:fixed;bottom:12px;right:12px;box-shadow:0 6px 24px #34246d22}.class-update .form-note{margin:0 0 8px}.class-update button+button{margin-left:18px}
@@ -0,0 +1,93 @@
1
+ (function(){let e=document.createElement(`link`).relList;if(e&&e.supports&&e.supports(`modulepreload`))return;for(let e of document.querySelectorAll(`link[rel="modulepreload"]`))n(e);new MutationObserver(e=>{for(let t of e)if(t.type===`childList`)for(let e of t.addedNodes)e.tagName===`LINK`&&e.rel===`modulepreload`&&n(e)}).observe(document,{childList:!0,subtree:!0});function t(e){let t={};return e.integrity&&(t.integrity=e.integrity),e.referrerPolicy&&(t.referrerPolicy=e.referrerPolicy),t.credentials=e.crossOrigin===`use-credentials`?`include`:e.crossOrigin===`anonymous`?`omit`:`same-origin`,t}function n(e){if(e.ep)return;e.ep=!0;let n=t(e);fetch(e.href,n)}})();function e(e){if(typeof e!=`string`||e.length>2048)return null;try{let t=new URL(e);return t.protocol!==`https:`||t.username||t.password?null:t.href}catch{return null}}function t(n){let r=e(n);if(!r)return null;let i=new URL(r);if(!i.port&&(i.hostname===`pairux.com`||i.hostname===`www.pairux.com`)){let e=/^\/(?:l|join|embed)\/([A-Za-z0-9_-]{1,64})\/?$/.exec(i.pathname)?.[1];return e?{provider:`pairux`,url:`https://pairux.com/l/${e}`,embed:`https://pairux.com/embed/${e}`,join:`https://pairux.com/join/${e}`}:null}if(!i.port&&(i.hostname===`nixamp.com`||i.hostname===`www.nixamp.com`)){if(i.pathname!==`/`)return null;let t=i.searchParams.get(`url`)||``,n=i.searchParams.get(`play`)||(t?`live`:``);if(/\/admin\//i.test(t)||/\/admin\//i.test(n)||i.searchParams.has(`key`)||t&&!e(t)||!/^(?:live|channel:[^\s]+|track:\d+)$/.test(n)&&!e(n))return null;let r=new URL(`https://nixamp.com/`);t&&r.searchParams.set(`url`,t),r.searchParams.set(`play`,n);let a=r.href;return r.searchParams.set(`embed`,`1`),{provider:`nixamp`,url:a,embed:r.href,join:a}}if(i.hostname.endsWith(`.nixamp.com`)&&/^\/view\/[A-Za-z0-9_-]+\/?$/.test(i.pathname)&&!i.search){let e=new URL(`https://nixamp.com/`);return e.searchParams.set(`url`,i.href),e.searchParams.set(`play`,`live`),t(e.href)}return null}var n=class extends Error{status;constructor(e,t){super(e),this.status=t}};async function r(e,t={}){let r=await fetch(e,{credentials:`same-origin`,...t,headers:{...t.body?{"content-type":`application/json`}:{},...t.headers}}),i={};try{i=await r.json()}catch{i={}}if(!r.ok)throw new n(String((i&&typeof i==`object`?i:{}).error??`That did not work.`),r.status);return i}function i(e,t,n=`POST`){return r(e,{method:n,body:JSON.stringify(t)})}var a=document.querySelector(`#main`),o=document.querySelector(`#account-button`),s=document.querySelector(`#account-dialog`),c=document.querySelector(`#account-form`),l=document.querySelector(`#account-title`),u=document.querySelector(`#account-copy`),d=document.querySelector(`#account-error`),f=document.querySelector(`#account-mode`),p=document.querySelector(`#event-dialog`),m=document.querySelector(`#event-form`),h=document.querySelector(`#event-form-title`),g=document.querySelector(`#event-kicker`),_=document.querySelector(`#event-submit`),v=document.querySelector(`#event-error`),y=document.querySelector(`#schedule-fields`),b=null,x=!1,S=!1,C=null,w=!1,T=null,E=null,D=null,O=new WeakMap;function k(e,t){O.get(e)===t||e.contains(document.activeElement)||(e.innerHTML=t,O.set(e,t))}function A(e){return String(e??``).replaceAll(`&`,`&amp;`).replaceAll(`<`,`&lt;`).replaceAll(`>`,`&gt;`).replaceAll(`"`,`&quot;`).replaceAll(`'`,`&#039;`)}function j(e){let t=document.querySelector(`#event-update`);t&&t.textContent!==e&&(t.textContent=e),document.querySelector(`.class-update`)?.removeAttribute(`hidden`)}function M(e){return`/live/${encodeURIComponent(e.slug)}`}function N(){let e=new URLSearchParams(location.search).get(`invite`);return e?`?invite=${encodeURIComponent(e)}`:``}function P(e,t={}){return e?new Intl.DateTimeFormat(void 0,{weekday:`short`,month:`short`,day:`numeric`,hour:`numeric`,minute:`2-digit`,...t}).format(new Date(e)):`Time to be announced`}function F(e){return e.status===`live`?`Live now`:e.status===`ended`?e.recordingId?`Replay available`:`Event ended`:e.status===`cancelled`?`Cancelled`:P(e.startsAt)}function I(e){return D?.permissions.includes(e)??!1}function L(){o.textContent=b?b.email.split(`@`)[0]||`Account`:`Sign in`,o.classList.toggle(`signed-in`,!!b)}async function R(){try{b=(await r(`/api/v1/auth/me`)).account}catch{b=null}L()}function z(e){if(b){s.showModal(),l.textContent=`Signed in as ${b.email}`,u.textContent=`Your BackToSchool identity is your NixAmp account.`,c.hidden=!0;return}T=e??null,c.hidden=!1,d.textContent=``,B(),s.showModal()}function B(){l.textContent=x?`Create your account`:`Welcome back`,u.textContent=x?`One NixAmp account works here and everywhere NixAmp goes.`:`Sign in to host, chat, or raise your hand.`,f.textContent=x?`Already have an account? Sign in`:`New here? Create an account`,document.querySelector(`#account-forgot`).hidden=x;let e=c.elements.namedItem(`password`);e.autocomplete=x?`new-password`:`current-password`,c.querySelector(`button[type="submit"]`).textContent=x?`Create account`:`Sign in`}function V(e){let t=new Date(e);return`${t.getFullYear()}-${String(t.getMonth()+1).padStart(2,`0`)}-${String(t.getDate()).padStart(2,`0`)}T${String(t.getHours()).padStart(2,`0`)}:${String(t.getMinutes()).padStart(2,`0`)}`}function H(e,t=null){if(!b){z(()=>H(e,t));return}if(w)return;C=t,m.reset(),S=e===`scheduled`,y.hidden=!S,h.textContent=t?`Edit event`:S?`Schedule a class`:`Go live`,g.textContent=t?`Your classroom`:`Teach something live`,_.textContent=t?`Save changes`:S?`Schedule class`:`Create live`,v.textContent=``;let n=m.elements.namedItem(`startsAt`);if(n.required=S&&(!t||!!t.startsAt),t){for(let e of[`title`,`description`,`topic`,`visibility`,`broadcastUrl`,`hostName`,`homepageUrl`,`avatarUrl`,`recurrence`])m.elements.namedItem(e).value=t[e]??(e===`recurrence`?`none`:``);for(let e of[`chatEnabled`,`handRaiseEnabled`])m.elements.namedItem(e).checked=t[e];t.startsAt&&(n.value=V(t.startsAt));let e=m.elements.namedItem(`expectedDurationMinutes`),r=String(t.expectedDurationMinutes??60);[...e.options].some(e=>e.value===r)||e.add(new Option(`${r} minutes`,r)),e.value=r}else if(S){let e=new Date(Date.now()+36e5);e.setMinutes(Math.ceil(e.getMinutes()/15)*15,0,0),n.value=V(e.toISOString())}let r=Intl.DateTimeFormat().resolvedOptions().timeZone||`UTC`;document.querySelector(`#schedule-timezone`).textContent=`Date and time are shown in ${r}.`+(t?.recurrence?` This class repeats in ${t.timezone}.`:``),p.showModal()}function U(e){history.pushState(null,``,e),$()}function W(e){let t=e.status===`live`||e.status===`starting`;return`
2
+ <article class="event-card ${t?`is-live`:``}">
3
+ <a href="${M(e)}" data-link>
4
+ <div class="event-card-top">
5
+ <span class="status-pill ${t?`status-live`:``}">${t?`Live`:A(F(e))}</span>
6
+ ${e.topic?`<span class="topic">${A(e.topic)}</span>`:``}
7
+ </div>
8
+ <h3>${A(e.title)}</h3>
9
+ <p>${A(e.description||`Drop in and learn something with us.`)}</p>
10
+ <span class="event-link">${t?`Watch live`:`See event`}<span aria-hidden="true">→</span></span>
11
+ </a>
12
+ </article>`}async function G(){document.title=`BackToSchool.help — Learn something live`;let{events:e}=await r(`/api/v1/events?limit=60`).catch(()=>({events:[]})),t=e.filter(e=>e.status===`live`||e.status===`starting`),n=e.filter(e=>e.status===`scheduled`||e.status===`draft`),i=e.filter(e=>e.status===`ended`&&e.recordingId);a.innerHTML=`
13
+ <section class="hero">
14
+ <div class="hero-copy">
15
+ <span class="eyebrow">Go back to school whenever you want</span>
16
+ <h1>Learn something<br /><em>live.</em></h1>
17
+ <p>Watch a live lesson, follow a screen share, and ask questions as you learn.</p>
18
+ <div class="hero-actions">
19
+ <button class="button button-primary" type="button" data-event-mode="live"><span class="live-dot"></span> Go live</button>
20
+ <button class="button button-secondary" type="button" data-event-mode="scheduled">Schedule live</button>
21
+ </div>
22
+ <p class="hero-note">Watch classes and screen shares. No account required.</p>
23
+ </div>
24
+ <div class="hero-board" aria-label="What is happening today">
25
+ <span class="tape tape-one" aria-hidden="true"></span>
26
+ <span class="tape tape-two" aria-hidden="true"></span>
27
+ <div class="board-rule"></div>
28
+ <span class="board-label">Today’s lesson</span>
29
+ <strong>${A(t[0]?.title??n[0]?.title??`The best teachers are curious people`)}</strong>
30
+ <p>${t[0]?`Happening right now`:n[0]?F(n[0]):`Host the first live conversation`}</p>
31
+ ${t[0]?`<a href="${M(t[0])}" data-link class="board-action">Watch now <span>↗</span></a>`:`<button type="button" data-event-mode="live" class="board-action">Start a room <span>↗</span></button>`}
32
+ <div class="board-doodles" aria-hidden="true"><span>?</span><span>✦</span><span>≈</span></div>
33
+ </div>
34
+ </section>
35
+
36
+ <section class="event-section" id="live">
37
+ <div class="section-heading">
38
+ <div><span class="eyebrow">Walk in anytime</span><h2>Live now</h2></div>
39
+ <span class="section-count">${t.length} ${t.length===1?`room`:`rooms`}</span>
40
+ </div>
41
+ <div class="event-grid">
42
+ ${t.length?t.map(W).join(``):`<div class="empty-card"><span>☕</span><h3>The halls are quiet.</h3><p>Start a live and give people something worth dropping into.</p><button class="text-button" data-event-mode="live">Open a room →</button></div>`}
43
+ </div>
44
+ </section>
45
+
46
+ <section class="event-section event-section-tint" id="upcoming">
47
+ <div class="section-heading">
48
+ <div><span class="eyebrow">Save your seat</span><h2>Coming up</h2></div>
49
+ </div>
50
+ <div class="upcoming-list">
51
+ ${n.length?n.map(e=>`
52
+ <a class="upcoming-row" href="${M(e)}" data-link>
53
+ <time>${A(P(e.startsAt,{weekday:`short`,month:`short`,day:`numeric`}))}</time>
54
+ <span><strong>${A(e.title)}</strong><small>${A(e.topic||`Live conversation`)}</small></span>
55
+ <span class="row-arrow">→</span>
56
+ </a>`).join(``):`<p class="empty-line">Nothing scheduled yet. Your idea could be next.</p>`}
57
+ </div>
58
+ </section>
59
+
60
+ ${i.length?`<section class="event-section"><div class="section-heading"><div><span class="eyebrow">Listen after class</span><h2>Recent replays</h2></div></div><div class="event-grid">${i.map(W).join(``)}</div></section>`:``}
61
+
62
+ <section class="host-callout">
63
+ <span class="eyebrow">Know a thing or two?</span>
64
+ <h2>Someone wants to hear it.</h2>
65
+ <p>You don’t need a studio or a syllabus. Bring an idea and start talking.</p>
66
+ <button class="button button-light" type="button" data-event-mode="scheduled">Plan a conversation</button>
67
+ </section>`}function K(e){let n=t(e.broadcastUrl);if(n)return`<div class="classroom-video"><iframe src="${A(n.embed)}" title="${A(e.title)} broadcast" allow="autoplay; fullscreen; picture-in-picture; encrypted-media" allowfullscreen referrerpolicy="strict-origin-when-cross-origin"></iframe></div>
68
+ <p class="broadcast-caption"><a class="button button-secondary" href="${A(n.join)}" target="_blank" rel="noopener noreferrer">${n.provider===`pairux`?`Join screen share on Pairux`:`Open video in Nixamp`} ↗</a></p>`;let r=N(),i=e.recordingId&&e.status===`ended`?`/api/v1/recordings/${encodeURIComponent(e.recordingId)}`:`/api/channels/${encodeURIComponent(e.roomId)}${r}`,a=e.status===`live`||!!e.recordingId;return`
69
+ <div class="listen-card ${a?``:`waiting`}">
70
+ <div class="sound-orbit" aria-hidden="true"><span></span><span></span><span></span><b>▶</b></div>
71
+ <div>
72
+ <span class="eyebrow">${e.status===`live`?`On air now`:e.recordingId?`Replay`:`The room opens soon`}</span>
73
+ <h2>${a?e.status===`live`?`Listen live`:`Listen again`:`We’ll see you here.`}</h2>
74
+ <p id="player-note">${a?`Press play and you’re in. No sign-up, no setup.`:A(F(e))}</p>
75
+ ${a?`<button class="button button-primary listen-button" type="button" data-listen><span class="live-dot"></span> ${e.status===`live`?`Listen live`:`Play replay`}</button><audio id="event-audio" preload="none" src="${A(i)}" controls></audio>`:``}
76
+ </div>
77
+ </div>`}function q(e){return e.chatEnabled?`
78
+ <div id="chat-messages" class="chat-messages"><p class="muted">Loading the conversation…</p></div>
79
+ ${b?`<form id="chat-form" class="inline-form"><input name="body" maxlength="1000" placeholder="Add to the conversation" aria-label="Chat message" required /><button class="button button-small" type="submit">Send</button></form>`:`<button class="text-button" type="button" data-sign-in>Sign in to join the chat →</button>`}`:`<div class="panel-empty">Chat is off for this event.</div>`}function J(e,t){switch(e){case`event-header`:return`<div class="event-heading"><span class="status-pill ${t.status===`live`?`status-live`:``}">${A(F(t))}</span>${t.topic?`<span class="topic">${A(t.topic)}</span>`:``}<h1>${A(t.title)}</h1><p>${A(t.description||`A live conversation powered by NixAmp.`)}</p></div>`;case`player`:return K(t);case`stage`:return`<div class="stage-card"><span class="stage-avatar">${A(t.title.slice(0,1).toUpperCase())}</span><div><span class="eyebrow">Host stage</span><h2>${A(t.title)}</h2><p id="broadcast-note">${t.status===`live`?`This event is live.`:`Start when you’re ready.`}</p></div></div>`;case`host`:return`<div class="host-line">${t.avatarUrl?`<img class="avatar host-avatar" src="${A(t.avatarUrl)}" alt="" referrerpolicy="no-referrer" />`:`<span class="avatar">${A((t.hostName||t.title).slice(0,1))}</span>`}<div><small>Your host</small><strong>${A(t.hostName||`Class host`)}</strong>${t.homepageUrl?`<a href="${A(t.homepageUrl)}" target="_blank" rel="noopener noreferrer">Visit homepage ↗</a>`:``}</div></div>`;case`about`:return`<p class="reading-copy">${A(t.description||`Come listen, learn, and ask a question live.`)}</p>${t.topic?`<span class="topic topic-large">${A(t.topic)}</span>`:``}`;case`join`:return b?`<p class="panel-empty">You’re signed in and ready to participate.</p>`:`<div class="join-line"><div><strong>Want to ask something?</strong><p>Join with your NixAmp account.</p></div><button class="button button-secondary" type="button" data-sign-in>Join in</button></div>`;case`chat`:return q(t);case`questions`:return`<div class="panel-empty">Questions shared in chat can be brought onto the stage.</div>`;case`resources`:return`<div class="panel-empty">The host hasn’t added resources yet.</div>`;case`participants`:return`<div class="metric"><strong id="listener-count">—</strong><span>listening now</span></div>`;case`speakers`:return`<div class="people-row"><span class="avatar">H</span><span><strong>Host</strong><small>On stage</small></span></div>`;case`raise-hand`:return t.handRaiseEnabled?`<div class="join-line"><div><strong>Have something to add?</strong><p>Let the host know you’d like to speak.</p></div><button class="button button-secondary" type="button" data-raise-hand>Raise hand</button></div>`:null;case`hand-raises`:return`<div id="hand-raises" class="hand-raises"><p class="muted">No hands raised.</p></div>`;case`invite`:return`<form id="invite-form" class="form-stack compact"><label>Email<input name="email" type="email" placeholder="someone@example.com" required /></label><label>Invite as<select name="role"><option value="listener">Listener</option><option value="speaker">Speaker</option><option value="moderator">Moderator</option></select></label><button class="button button-small" type="submit">Create invite</button><p id="invite-note" class="form-note"></p></form>`;case`share`:return`<div class="share-box"><input id="share-url" value="${A(location.href)}" readonly aria-label="Event URL" /><button class="button button-small" type="button" data-copy>Copy link</button></div>`;case`event-controls`:return`<div class="control-stack">
80
+ <a href="https://pairux.com/dashboard" target="_blank" rel="noopener noreferrer">Screen share with Pairux ↗</a>
81
+ <a href="https://nixamp.com/#files-panel" target="_blank" rel="noopener noreferrer">Upload &amp; broadcast in Nixamp ↗</a>
82
+ ${[`draft`,`scheduled`,`starting`].includes(t.status)?`<button class="button button-primary" type="button" data-start-event>Start class</button>`:``}
83
+ <button class="button button-danger" type="button" data-end-event ${t.status===`live`?``:`hidden`}>${t.recurrence?`End session & schedule next`:`End event`}</button>
84
+ <p id="broadcast-note" class="form-note" role="status">${t.broadcastUrl?`Start the broadcast in Pairux or Nixamp, then start the class here.`:`Add your broadcast link with Edit event.`}</p>
85
+ </div>`;case`schedule`:return`<div class="metric"><strong>${A(P(t.startsAt,{timeZone:t.timezone}))}</strong><span>${A(t.timezone)}${t.recurrence?` · Repeats ${t.recurrence}`:``}</span></div>`;case`recording`:return`<div class="control-line"><span>${t.recordingEnabled?`Recording requested`:`Recording is off`}</span><span class="status-dot ${t.recordingEnabled?`on`:``}"></span></div>`;default:return null}}async function Y(e){let t=await r(`/api/v1/events/${encodeURIComponent(e)}${N()}`);E?.(),E=null,D=t;let n=t.event;document.title=`${n.title} — BackToSchool.help`,document.querySelector(`meta[name="description"]`)?.setAttribute(`content`,n.description||`Listen to ${n.title} live on BackToSchool.help.`),document.querySelector(`meta[property="og:title"]`)?.setAttribute(`content`,n.title),document.querySelector(`meta[property="og:description"]`)?.setAttribute(`content`,n.description||`Learn something live.`);let i=(e,t)=>`<section class="event-panel panel-${e}"><h2 class="panel-title">${t}</h2>${J(e,n)??``}</section>`;a.innerHTML=`
86
+ <div class="event-shell">
87
+ <div class="event-toolbar"><a class="back-link" href="/" data-link>← Explore more lives</a>${b?.id===n.ownerId?`<button class="button button-secondary" type="button" data-edit-event>Edit event</button>`:``}</div>
88
+ <div class="class-update" hidden><p id="event-update" role="status" class="form-note"></p><button class="text-button" data-refresh-event>Refresh class details</button><button class="text-button" data-dismiss-update>Dismiss</button></div>
89
+ <div class="event-layout">
90
+ <div class="region region-primary">${i(`event-header`,`Live classroom`)}${i(`player`,`Watch class`)}${i(`chat`,`Class chat`)}</div>
91
+ <aside class="region region-secondary">${i(`host`,`Meet your host`)}${i(`schedule`,`Schedule`)}${b?.id===n.ownerId?i(`event-controls`,`Host controls`):i(`join`,`Join the class`)}${b?.id===n.ownerId&&n.visibility===`private`?`<details class="event-panel"><summary>Invite students</summary>${J(`invite`,n)}</details>`:``}${i(`share`,`Share class`)}${b?.id===n.ownerId&&n.handRaiseEnabled?i(`hand-raises`,`Raised hands`):n.handRaiseEnabled?i(`raise-hand`,`Ask a question`):``}</aside>
92
+ </div>
93
+ </div>`,X(n)}function X(e){document.querySelector(`[data-listen]`)?.addEventListener(`click`,async e=>{let t=e.currentTarget,n=document.querySelector(`#event-audio`);if(n){t.disabled=!0,t.textContent=`Connecting…`;try{await n.play(),t.textContent=`You’re listening`,document.querySelector(`#player-note`).textContent=`Live audio from this NixAmp room.`}catch{t.disabled=!1,t.textContent=`Try listening again`,document.querySelector(`#player-note`).textContent=`The host may still be getting ready. Try again in a moment.`}}});let t=document.querySelector(`#chat-form`);t?.addEventListener(`submit`,n=>{n.preventDefault();let r=t.elements.namedItem(`body`),a=r.value.trim();a&&(r.value=``,i(`/api/v1/events/${encodeURIComponent(e.id)}/chat`,{body:a}).then(()=>Z(e)).catch(e=>{r.placeholder=e instanceof Error?e.message:`Could not send`}))});let a=document.querySelector(`#invite-form`);a?.addEventListener(`submit`,t=>{t.preventDefault();let n=new FormData(a),i=document.querySelector(`#invite-note`);i.textContent=`Creating invite…`,r(`/api/v1/events/${encodeURIComponent(e.id)}/invitations`,{method:`POST`,body:JSON.stringify({email:n.get(`email`),role:n.get(`role`)})}).then(async({inviteUrl:e,sent:t})=>{await navigator.clipboard?.writeText(e).catch(()=>void 0),i.textContent=t?`Invite sent. The link is copied too.`:`Invite link copied: ${e}`,a.reset()}).catch(e=>{i.textContent=e instanceof Error?e.message:`Could not invite`})}),document.querySelector(`[data-raise-hand]`)?.addEventListener(`click`,t=>{let r=t.currentTarget;r.disabled=!0,i(`/api/v1/events/${encodeURIComponent(e.id)}/hand-raises`,{}).then(()=>{r.textContent=`Hand raised ✓`}).catch(e=>{r.disabled=!1,r.textContent=e instanceof n&&e.status===401?`Sign in to raise hand`:`Try again`})}),document.querySelector(`[data-edit-event]`)?.addEventListener(`click`,()=>H(`scheduled`,D.event)),document.querySelector(`[data-start-event]`)?.addEventListener(`click`,()=>void te()),document.querySelector(`[data-dismiss-update]`)?.addEventListener(`click`,()=>document.querySelector(`.class-update`).hidden=!0),document.querySelector(`[data-refresh-event]`)?.addEventListener(`click`,()=>U(M(e)+N())),document.querySelector(`[data-end-event]`)?.addEventListener(`click`,()=>void ne()),document.querySelector(`[data-copy]`)?.addEventListener(`click`,async e=>{await navigator.clipboard.writeText(location.href),e.currentTarget.textContent=`Copied ✓`});let o=0,s=0;e.chatEnabled&&(Z(e),o=window.setInterval(()=>void Z(e),4e3),s=I(`event.moderate`)?window.setInterval(()=>void Q(e),3e3):0,Q(e));let c=e.version,l=window.setInterval(()=>void r(`/api/v1/events/${encodeURIComponent(e.id)}${N()}`).then(e=>{if(e.event.version===D?.event.version||e.event.version===c)return;c=e.event.version;let t=document.querySelector(`.event-heading .status-pill`),n=F(e.event);t&&t.textContent!==n&&(t.textContent=n,t.classList.toggle(`status-live`,e.event.status===`live`)),j(`The host updated this class. Refresh the details when you’re ready.`)}).catch(()=>void 0),5e3);E=()=>{clearInterval(o),clearInterval(s),clearInterval(l)},ee(e)}async function Z(e){let t=document.querySelector(`#chat-messages`);if(t)try{let{messages:n}=await r(`/api/v1/events/${encodeURIComponent(e.id)}/chat${N()}`);k(t,n.length?n.map(e=>`<div class="chat-message"><span class="avatar avatar-small">${A(e.authorName.slice(0,1).toUpperCase())}</span><p><strong>${A(e.authorName)}</strong><span>${A(e.body)}</span></p></div>`).join(``):`<p class="muted">No messages yet. Say hello when you’re ready.</p>`)}catch{t.innerHTML=`<p class="muted">Chat is taking a break. You can still watch the class.</p>`}}async function Q(e){let t=document.querySelector(`#hand-raises`);if(t&&I(`event.moderate`))try{let{handRaises:n}=await r(`/api/v1/events/${encodeURIComponent(e.id)}/hand-raises`);if(t.contains(document.activeElement))return;k(t,n.length?n.map(e=>`<div class="raise-row"><span><strong>${A(e.displayName)}</strong><small>${A(e.state)}</small></span><div><button class="text-button" data-hand="${A(e.accountId)}" data-state="invited">Invite</button><button class="text-button" data-hand="${A(e.accountId)}" data-state="dismissed">Dismiss</button></div></div>`).join(``):`<p class="muted">No hands raised.</p>`)}catch{t.contains(document.activeElement)||(t.innerHTML=`<p class="muted">Could not refresh hand raises.</p>`)}}async function ee(e){let t=document.querySelector(`#listener-count`);if(t)try{let{channels:n}=await r(`/api/channels`);t.textContent=String(n.find(t=>t.id===e.roomId)?.listeners??0)}catch{t.textContent=`0`}}async function te(){if(!D)return;let e=document.querySelector(`#broadcast-note`),t=document.querySelector(`[data-start-event]`);if(!D.event.broadcastUrl){e&&(e.textContent=`Add a Pairux or Nixamp broadcast link with Edit event first.`);return}if(t?.getAttribute(`aria-disabled`)!==`true`){t?.setAttribute(`aria-disabled`,`true`);try{D=await r(`/api/v1/events/${encodeURIComponent(D.event.id)}/start`,{method:`POST`,body:JSON.stringify({version:D.event.version})}),t&&(t.textContent=`Class is live`),document.querySelector(`[data-end-event]`)?.removeAttribute(`hidden`);let n=document.querySelector(`.event-heading .status-pill`);n&&(n.textContent=`Live now`,n.classList.add(`status-live`)),e&&(e.textContent=`Your class is live. Viewers can join using this page.`)}catch(n){t?.removeAttribute(`aria-disabled`),e&&(e.textContent=n instanceof Error?n.message:`Could not start the class.`)}}}async function ne(){let e=document.querySelector(`[data-end-event]`);if(!D||e?.getAttribute(`aria-disabled`)===`true`)return;e?.setAttribute(`aria-disabled`,`true`);let t=D.event;try{let e=await r(`/api/v1/events/${encodeURIComponent(t.id)}/end`,{method:`POST`,body:JSON.stringify({version:t.version})});D=e;let n=document.querySelector(`.event-heading .status-pill`);n&&(n.textContent=F(e.event),n.classList.remove(`status-live`)),await fetch(`/api/channels/${encodeURIComponent(t.roomId)}`,{method:`DELETE`,credentials:`same-origin`});let i=document.querySelector(`#broadcast-note`);i&&(i.textContent=e.event.recurrence?`Session ended. Next class: ${P(e.event.startsAt)}. End the broadcast in Pairux or Nixamp too.`:`Event ended. End the broadcast in Pairux or Nixamp too.`);let a=document.querySelector(`[data-end-event]`);a&&(a.textContent=`Session ended`,a.setAttribute(`aria-disabled`,`true`)),j(`This session has ended. Refresh to see the latest schedule.`)}catch(t){e?.removeAttribute(`aria-disabled`);let n=document.querySelector(`#broadcast-note`);n&&(n.textContent=t instanceof Error?t.message:`Could not end the event.`)}}async function $(){E?.(),E=null,D=null;let e=/^\/live\/([^/]+)\/?$/.exec(location.pathname);try{e?await Y(decodeURIComponent(e[1])):await G()}catch(e){a.innerHTML=`<section class="error-page"><span class="eyebrow">Class dismissed?</span><h1>${A(e instanceof n&&e.status===404?`That live event isn’t available.`:`We couldn’t open this page.`)}</h1><p>${A(e instanceof Error?e.message:`Try again in a moment.`)}</p><a href="/" data-link class="button button-primary">Back to live events</a></section>`}}o.addEventListener(`click`,()=>{if(!b){z();return}confirm(`Signed in as ${b.email}. Sign out?`)&&r(`/api/v1/auth/logout`,{method:`POST`}).finally(()=>{b=null,L(),$()})}),f.addEventListener(`click`,()=>{x=!x,d.textContent=``,B()}),c.addEventListener(`submit`,e=>{e.preventDefault();let t=new FormData(c),n=c.querySelector(`button[type="submit"]`);n.disabled=!0,d.textContent=``,r(`/api/v1/auth/${x?`signup`:`login`}`,{method:`POST`,body:JSON.stringify({email:t.get(`email`),password:t.get(`password`)})}).then(e=>{b=e.account,L(),c.reset(),s.close();let t=T;T=null,t?t():$()}).catch(e=>{d.textContent=e instanceof Error?e.message:`That did not work.`}).finally(()=>{n.disabled=!1})}),m.querySelector(`[name="recurrence"]`)?.addEventListener(`change`,()=>{m.elements.namedItem(`recurrence`).value!==`none`&&(S=!0,y.hidden=!1,m.elements.namedItem(`startsAt`).required=!0)}),m.addEventListener(`submit`,e=>{if(e.preventDefault(),w)return;let t=new FormData(m),n=C,i=String(t.get(`startsAt`)??``),a=!n?.startsAt||i!==V(n.startsAt),o=String(t.get(`recurrence`));if(o!==`none`&&!i&&!S){S=!0,y.hidden=!1,m.elements.namedItem(`startsAt`).required=!0,v.textContent=`Choose the first class date and time for this recurring schedule.`;return}w=!0,_.setAttribute(`aria-disabled`,`true`),m.setAttribute(`aria-busy`,`true`),v.textContent=``;let s={title:t.get(`title`),description:t.get(`description`),topic:t.get(`topic`),visibility:t.get(`visibility`),broadcastUrl:t.get(`broadcastUrl`),hostName:t.get(`hostName`),homepageUrl:t.get(`homepageUrl`),avatarUrl:t.get(`avatarUrl`),recurrence:o,chatEnabled:t.get(`chatEnabled`)===`on`,handRaiseEnabled:t.get(`handRaiseEnabled`)===`on`,...n?{version:n.version}:{kind:`class`},...S?{startsAt:i?a?new Date(i).toISOString():n.startsAt:null,timezone:a?Intl.DateTimeFormat().resolvedOptions().timeZone||`UTC`:n.timezone,expectedDurationMinutes:Number(t.get(`expectedDurationMinutes`))}:{}};r(n?`/api/v1/events/${encodeURIComponent(n.id)}`:`/api/v1/events`,{method:n?`PATCH`:`POST`,body:JSON.stringify(s)}).then(async e=>{n?(await Y(e.event.slug),p.close(),document.querySelector(`[data-edit-event]`)?.focus({preventScroll:!0})):(p.close(),U(M(e.event))),m.reset(),C=null}).catch(e=>{v.textContent=e instanceof Error?e.message:`Could not save the event.`}).finally(()=>{w=!1,_.removeAttribute(`aria-disabled`),m.removeAttribute(`aria-busy`)})}),document.addEventListener(`click`,e=>{let t=e.target,n=t.closest(`a[data-link]`);if(n&&n.origin===location.origin){e.preventDefault(),U(`${n.pathname}${n.search}`);return}let r=t.closest(`[data-event-mode]`)?.dataset.eventMode;(r===`live`||r===`scheduled`)&&H(r),t.closest(`[data-sign-in]`)&&z(()=>void $());let a=t.closest(`[data-close]`);a&&a.closest(`dialog`)?.close();let o=t.closest(`[data-hand]`);o&&D&&(o.disabled=!0,i(`/api/v1/events/${encodeURIComponent(D.event.id)}/hand-raises/${encodeURIComponent(o.dataset.hand??``)}`,{state:o.dataset.state},`PATCH`).then(()=>Q(D.event)).finally(()=>{o.disabled=!1}))}),window.addEventListener(`popstate`,()=>void $()),R().then(()=>$());
@@ -4,7 +4,7 @@
4
4
  <meta charset="UTF-8" />
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
6
6
  <meta name="theme-color" content="#34246d" />
7
- <meta name="description" content="Learn something live. Listen to public conversations, classes, and talks powered by NixAmp." />
7
+ <meta name="description" content="Learn something live. Watch live classes, screen shares, and talks powered by NixAmp." />
8
8
  <meta property="og:site_name" content="BackToSchool.help" />
9
9
  <meta property="og:title" content="BackToSchool.help — Learn something live" />
10
10
  <meta property="og:description" content="Live conversations with people who know." />
@@ -12,8 +12,8 @@
12
12
  <link rel="manifest" href="/manifest.webmanifest" />
13
13
  <link rel="icon" href="/icon.svg" type="image/svg+xml" />
14
14
  <title>BackToSchool.help — Learn something live</title>
15
- <script type="module" crossorigin src="/assets/index-D7Oas1m5.js"></script>
16
- <link rel="stylesheet" crossorigin href="/assets/index-6vtCMzi1.css">
15
+ <script type="module" crossorigin src="/assets/index-pQ_uH4bZ.js"></script>
16
+ <link rel="stylesheet" crossorigin href="/assets/index-OE2xWZUH.css">
17
17
  </head>
18
18
  <body>
19
19
  <div id="app">
@@ -36,12 +36,12 @@
36
36
  </section>
37
37
  </main>
38
38
  <footer class="site-footer">
39
- <span>Live audio, made simple.</span>
39
+ <span>Live classes. Screen sharing. Learn together.</span>
40
40
  <span>Powered by <a href="https://nixamp.com">NixAmp</a></span>
41
41
  </footer>
42
42
  </div>
43
43
 
44
- <dialog id="account-dialog" class="dialog">
44
+ <dialog id="account-dialog" class="dialog" aria-labelledby="account-title">
45
45
  <button class="dialog-close" type="button" data-close aria-label="Close">×</button>
46
46
  <span class="eyebrow">Your NixAmp account</span>
47
47
  <h2 id="account-title">Welcome back</h2>
@@ -51,20 +51,28 @@
51
51
  <label>Password<input name="password" type="password" autocomplete="current-password" minlength="8" required /></label>
52
52
  <p class="form-error" id="account-error" role="alert"></p>
53
53
  <button class="button button-primary" type="submit">Sign in</button>
54
+ <a class="text-button" id="account-forgot" href="/reset-password">Forgot password?</a>
54
55
  <button class="text-button" id="account-mode" type="button">New here? Create an account</button>
55
56
  </form>
56
57
  </dialog>
57
58
 
58
- <dialog id="event-dialog" class="dialog dialog-wide">
59
+ <dialog id="event-dialog" class="dialog dialog-wide" aria-labelledby="event-form-title">
59
60
  <button class="dialog-close" type="button" data-close aria-label="Close">×</button>
60
61
  <span class="eyebrow" id="event-kicker">Start a conversation</span>
61
62
  <h2 id="event-form-title">Go live</h2>
62
63
  <form id="event-form" class="form-stack">
64
+ <fieldset class="broadcast-options">
65
+ <legend>Broadcast your class</legend>
66
+ <p>Share your screen with Pairux, or upload a file and go live in Nixamp. Paste the share link below.</p>
67
+ <div class="broadcast-links"><a class="button button-secondary" href="https://pairux.com/dashboard" target="_blank" rel="noopener noreferrer">Screen share with Pairux ↗</a><a class="button button-secondary" href="https://nixamp.com/#files-panel" target="_blank" rel="noopener noreferrer">Upload &amp; broadcast in Nixamp ↗</a></div>
68
+ <label>Broadcast link<input name="broadcastUrl" type="url" maxlength="2048" placeholder="Pairux join link or Nixamp Share link" aria-describedby="broadcast-help" /></label>
69
+ <p class="form-note" id="broadcast-help">You can add the link now or edit your event when the broadcast is ready.</p>
70
+ </fieldset>
63
71
  <label>Title<input name="title" maxlength="160" placeholder="What will people learn?" required /></label>
64
- <label>Description<textarea name="description" maxlength="5000" rows="3" placeholder="A short introduction for listeners"></textarea></label>
72
+ <label>Description<textarea name="description" maxlength="5000" rows="3" placeholder="What will students learn?"></textarea></label>
65
73
  <div class="form-pair">
66
74
  <label>Topic<input name="topic" maxlength="100" placeholder="Technology, music…" /></label>
67
- <label>Who can listen
75
+ <label>Who can join
68
76
  <select name="visibility">
69
77
  <option value="public">Public</option>
70
78
  <option value="unlisted">Anyone with the link</option>
@@ -83,10 +91,18 @@
83
91
  </select>
84
92
  </label>
85
93
  </div>
94
+ <p class="form-note" id="schedule-timezone"></p>
95
+ <label for="event-recurrence">Repeat</label><select id="event-recurrence" name="recurrence"><option value="none">Does not repeat</option><option value="daily">Every day</option><option value="weekly">Every week</option></select>
96
+ <p class="form-note">Repeating classes keep this same link. Ending a session schedules the next one at the same local time.</p>
97
+ <fieldset class="broadcast-options"><legend>Host profile</legend>
98
+ <label>Host name<input name="hostName" maxlength="100" autocomplete="name" /></label>
99
+ <label>Homepage<input name="homepageUrl" type="url" maxlength="2048" placeholder="https://your-site.com" /></label>
100
+ <label>Profile photo URL<input name="avatarUrl" type="url" maxlength="2048" placeholder="https://your-site.com/photo.jpg" /></label>
101
+ </fieldset>
86
102
  <div class="form-checks">
87
103
  <label><input name="chatEnabled" type="checkbox" checked /> Chat</label>
88
104
  <label><input name="handRaiseEnabled" type="checkbox" checked /> Raise hand</label>
89
- <label><input name="recordingEnabled" type="checkbox" /> Record</label>
105
+
90
106
  </div>
91
107
  <p class="form-error" id="event-error" role="alert"></p>
92
108
  <button class="button button-primary" type="submit" id="event-submit">Create live</button>
@@ -1,3 +1,4 @@
1
+ import { PasswordResets } from "./password-reset.ts";
1
2
  import { type Identity, type Users } from "./oauth.ts";
2
3
  import type { Queryable } from "./follows.ts";
3
4
  import { type IssuedToken, type TokenKind, type TokenRecord, Tokens } from "./tokens.ts";
@@ -69,6 +70,7 @@ export declare class Accounts {
69
70
  private readonly system;
70
71
  /** Null only where a test injected an auth system and no storage. */
71
72
  readonly tokens: Tokens | null;
73
+ readonly passwordResets: PasswordResets | null;
72
74
  private readonly identities;
73
75
  constructor(options: AccountsOptions);
74
76
  signUp(email: unknown, password: unknown): Promise<AuthResult>;
package/dist/accounts.js CHANGED
@@ -19,7 +19,8 @@
19
19
  * terminal better than either -- OAuth 2.0 reached through the device grant in
20
20
  * device.ts -- and what it ends with is a token from tokens.ts.
21
21
  */
22
- import { createAuthSystem, PostgresAdapter } from "@profullstack/auth-system";
22
+ import { createAuthSystem, createPasswordUtils, PostgresAdapter } from "@profullstack/auth-system";
23
+ import { PasswordResets } from "./password-reset.js";
23
24
  import { Identities } from "./oauth.js";
24
25
  import { looksLikeToken, Tokens } from "./tokens.js";
25
26
  const NO_ACCOUNT = { ok: false, account: null, token: "", error: "" };
@@ -85,6 +86,7 @@ export class Accounts {
85
86
  system;
86
87
  /** Null only where a test injected an auth system and no storage. */
87
88
  tokens;
89
+ passwordResets;
88
90
  identities;
89
91
  constructor(options) {
90
92
  // The adapter is kept rather than only handed over: tokens and provider
@@ -105,6 +107,8 @@ export class Accounts {
105
107
  passwordOptions: { ...PASSWORD_RULES },
106
108
  });
107
109
  this.tokens = adapter ? new Tokens(adapter) : null;
110
+ this.passwordResets = adapter && this.tokens
111
+ ? new PasswordResets(adapter, this.tokens, createPasswordUtils({ ...PASSWORD_RULES })) : null;
108
112
  this.identities = adapter ? new Identities(adapter, adapter) : null;
109
113
  }
110
114
  async signUp(email, password) {
@@ -173,7 +177,10 @@ export class Accounts {
173
177
  }
174
178
  }
175
179
  try {
176
- return readClaims(await this.system.validateToken(token));
180
+ const account = readClaims(await this.system.validateToken(token));
181
+ if (account && this.passwordResets && !(await this.passwordResets.acceptsLegacyToken(account.id, token)))
182
+ return null;
183
+ return account;
177
184
  }
178
185
  catch {
179
186
  return null;
@@ -0,0 +1,15 @@
1
+ /** Public classroom metadata, shared by the API and the small school client. */
2
+ export interface ClassroomSettings {
3
+ broadcastUrl?: string;
4
+ hostName?: string;
5
+ homepageUrl?: string;
6
+ avatarUrl?: string;
7
+ recurrence?: "daily" | "weekly";
8
+ }
9
+ export declare function publicWebUrl(value: unknown): string | null;
10
+ export declare function classroomBroadcast(value: unknown): {
11
+ provider: "pairux" | "nixamp";
12
+ url: string;
13
+ embed: string;
14
+ join: string;
15
+ } | null;
@@ -0,0 +1,54 @@
1
+ export function publicWebUrl(value) {
2
+ if (typeof value !== "string" || value.length > 2048)
3
+ return null;
4
+ try {
5
+ const url = new URL(value);
6
+ if (url.protocol !== "https:" || url.username || url.password)
7
+ return null;
8
+ return url.href;
9
+ }
10
+ catch {
11
+ return null;
12
+ }
13
+ }
14
+ export function classroomBroadcast(value) {
15
+ const safe = publicWebUrl(value);
16
+ if (!safe)
17
+ return null;
18
+ const url = new URL(safe);
19
+ if (!url.port && (url.hostname === "pairux.com" || url.hostname === "www.pairux.com")) {
20
+ const code = /^\/(?:l|join|embed)\/([A-Za-z0-9_-]{1,64})\/?$/.exec(url.pathname)?.[1];
21
+ if (!code)
22
+ return null;
23
+ return { provider: "pairux", url: `https://pairux.com/l/${code}`, embed: `https://pairux.com/embed/${code}`, join: `https://pairux.com/join/${code}` };
24
+ }
25
+ if (!url.port && (url.hostname === "nixamp.com" || url.hostname === "www.nixamp.com")) {
26
+ if (url.pathname !== "/")
27
+ return null;
28
+ // Never publish an admin credential in a classroom share link.
29
+ const server = url.searchParams.get("url") || "";
30
+ // The server's Share panel supplies a viewer URL without a track selection.
31
+ const play = url.searchParams.get("play") || (server ? "live" : "");
32
+ if (/\/admin\//i.test(server) || /\/admin\//i.test(play) || url.searchParams.has("key"))
33
+ return null;
34
+ if (server && !publicWebUrl(server))
35
+ return null;
36
+ if (!/^(?:live|channel:[^\s]+|track:\d+)$/.test(play) && !publicWebUrl(play))
37
+ return null;
38
+ const clean = new URL("https://nixamp.com/");
39
+ if (server)
40
+ clean.searchParams.set("url", server);
41
+ clean.searchParams.set("play", play);
42
+ const original = clean.href;
43
+ clean.searchParams.set("embed", "1");
44
+ return { provider: "nixamp", url: original, embed: clean.href, join: original };
45
+ }
46
+ // A self-hosted Nixamp viewer link can be played through the shared client.
47
+ if (url.hostname.endsWith(".nixamp.com") && /^\/view\/[A-Za-z0-9_-]+\/?$/.test(url.pathname) && !url.search) {
48
+ const player = new URL("https://nixamp.com/");
49
+ player.searchParams.set("url", url.href);
50
+ player.searchParams.set("play", "live");
51
+ return classroomBroadcast(player.href);
52
+ }
53
+ return null;
54
+ }
@@ -1,3 +1,4 @@
1
+ import { type ClassroomSettings } from "./classroom.ts";
1
2
  import type { Queryable } from "./follows.ts";
2
3
  export declare const LIVE_EVENT_STATUSES: readonly ["draft", "scheduled", "starting", "live", "encore", "ended", "cancelled", "archived"];
3
4
  export declare const LIVE_EVENT_VISIBILITIES: readonly ["public", "unlisted", "private"];
@@ -18,7 +19,7 @@ export type LiveEventVisibility = (typeof LIVE_EVENT_VISIBILITIES)[number];
18
19
  export type LiveEventKind = (typeof LIVE_EVENT_KINDS)[number];
19
20
  export type InvitationRole = (typeof INVITATION_ROLES)[number];
20
21
  export type InvitationState = (typeof INVITATION_STATES)[number];
21
- export interface LiveEvent {
22
+ export interface LiveEvent extends ClassroomSettings {
22
23
  id: string;
23
24
  slug: string;
24
25
  ownerId: string;
@@ -72,6 +73,11 @@ export interface CreatedInvitation extends EventInvitation {
72
73
  }
73
74
  export interface CreateLiveEventInput {
74
75
  title: unknown;
76
+ broadcastUrl?: unknown;
77
+ hostName?: unknown;
78
+ homepageUrl?: unknown;
79
+ avatarUrl?: unknown;
80
+ recurrence?: unknown;
75
81
  description?: unknown;
76
82
  topic?: unknown;
77
83
  kind?: unknown;