1<!DOCTYPE html> 2<html lang="en"> 3<head> 4<meta charset="UTF-8"> 5<meta name="viewport" content="width=device-width, initial-scale=1.0"> 6<title>Private Mentorship â Feminine Rising</title> 7<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32.png"> 8<link rel="icon" type="image/png" sizes="192x192" href="/favicon-192.png"> 9<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png"> 10<link rel="preconnect" href="https://fonts.googleapis.com"> 11<link href="https://fonts.googleapis.com/css2?family=Cormorant+Garamond:ital,wght@0,400;0,600;1,400&family=Lora:ital,wght@0,400;0,600;1,400&family=Marcellus&family=Great+Vibes&display=swap" rel="stylesheet">
12<script src="https://cdn.jsdelivr.net/npm/@supabase/supabase-js@2/dist/umd/supabase.min.js"></script>
vendor: 1 bytes, line 12
12
13<script src="/lame.min.js"></script>
13 14<style> 15/* ============================================ 16 DESIGN TOKENS 17 ============================================ */ 18:root { 19 --gold: #C5A55A; 20 --gold-light: #D4BA7A; 21 --gold-pale: #F0E6CC; 22 --plum: #8B2252; 23 --plum-deep: #6B1A42; 24 --berry: #940E4B; 25 --cream: #FBF8F3; 26 --cream-warm: #F7F0E5; 27 --charcoal: #2E2E2E; 28 --sidebar-width: 240px; 29} 30 31* { margin: 0; padding: 0; box-sizing: border-box; } 32 33body { 34 font-family: 'Lora', serif; 35 background: var(--cream); 36 color: var(--charcoal); 37 min-height: 100vh; 38} 39 40/* ============================================ 41 PASSWORD SCREEN 42 ============================================ */ 43#password-screen { 44 min-height: 100vh; 45 display: flex; 46 flex-direction: column; 47 align-items: center; 48 justify-content: center; 49 background: var(--cream); 50 position: relative; 51} 52 53#password-screen::before { 54 content: ''; 55 position: absolute; 56 top: 0; left: 0; right: 0; 57 height: 200px; 58 background: var(--plum); 59} 60 61.password-card { 62 position: relative; 63 background: var(--cream); 64 border: 1px solid var(--gold-light); 65 border-radius: 12px; 66 padding: 60px 50px 50px; 67 max-width: 460px; 68 width: 90%; 69 text-align: center; 70 box-shadow: 0 8px 40px rgba(139, 34, 82, 0.08); 71 margin-top: -40px; 72} 73 74.password-logo { width: 60px; height: 60px; margin-bottom: 20px; } 75 76.password-title { 77 font-family: 'Marcellus', serif; 78 font-size: 13px; 79 letter-spacing: 4px; 80 color: var(--gold); 81 text-transform: uppercase; 82 margin-bottom: 8px; 83} 84 85.password-heading { 86 font-family: 'Cormorant Garamond', serif; 87 font-size: 30px; 88 color: var(--plum-deep); 89 margin-bottom: 6px; 90 font-weight: 400; 91} 92 93.password-subtitle { 94 font-family: 'Cormorant Garamond', serif; 95 font-style: italic; 96 font-size: 16px; 97 color: var(--berry); 98 margin-bottom: 30px; 99} 100 101.password-divider { 102 display: flex; 103 align-items: center; 104 gap: 12px; 105 margin-bottom: 30px; 106} 107 108.password-divider::before, 109.password-divider::after { 110 content: ''; 111 flex: 1; 112 height: 1px; 113 background: var(--gold-light); 114} 115 116.password-divider span { color: var(--gold); font-size: 14px; } 117 118.password-prompt { 119 font-family: 'Cormorant Garamond', serif; 120 font-style: italic; 121 font-size: 15px; 122 color: var(--plum); 123 margin-bottom: 20px; 124} 125 126.password-input { 127 width: 100%; 128 padding: 14px 20px; 129 border: 1px solid var(--gold-light); 130 border-radius: 8px; 131 background: white; 132 font-family: 'Lora', serif; 133 font-size: 14px; 134 color: var(--charcoal); 135 text-align: center; 136 letter-spacing: 2px; 137 outline: none; 138 transition: border-color 0.3s; 139 margin-bottom: 16px; 140} 141 142.password-input:focus { 143 border-color: var(--gold); 144 box-shadow: 0 0 0 3px rgba(197, 165, 90, 0.15); 145} 146 147.password-input::placeholder { color: #bbb; letter-spacing: 1px; } 148 149.password-btn { 150 width: 100%; 151 padding: 14px; 152 background: var(--plum); 153 color: var(--gold); 154 border: none; 155 border-radius: 8px; 156 font-family: 'Marcellus', serif; 157 font-size: 13px; 158 letter-spacing: 3px; 159 text-transform: uppercase; 160 cursor: pointer; 161 transition: all 0.3s; 162} 163 164.password-btn:hover { 165 background: var(--plum-deep); 166 box-shadow: 0 4px 16px rgba(107, 26, 66, 0.3); 167} 168 169.password-error { 170 color: var(--berry); 171 font-size: 13px; 172 font-style: italic; 173 margin-top: 12px; 174 opacity: 0; 175 transition: opacity 0.3s; 176} 177 178.password-error.show { opacity: 1; } 179 180.back-link { 181 position: relative; 182 margin-top: 20px; 183 font-size: 12px; 184 color: var(--gold); 185 text-decoration: none; 186 font-family: 'Marcellus', serif; 187 letter-spacing: 2px; 188 text-transform: uppercase; 189} 190 191.back-link:hover { color: var(--plum); } 192 193.back-to-portal { 194 position: fixed; 195 top: 16px; 196 left: 20px; 197 display: inline-flex; 198 align-items: center; 199 gap: 6px; 200 padding: 8px 18px; 201 background: rgba(107, 26, 66, 0.7); 202 border: 1px solid rgba(197, 165, 90, 0.4); 203 border-radius: 6px; 204 color: var(--gold-light); 205 font-family: 'Marcellus', serif; 206 font-size: 10px; 207 letter-spacing: 2px; 208 text-transform: uppercase; 209 text-decoration: none; 210 transition: all 0.3s; 211 z-index: 300; 212} 213 214.back-to-portal:hover { 215 background: rgba(107, 26, 66, 0.95); 216 border-color: var(--gold); 217 color: var(--gold); 218} 219 220/* ============================================ 221 MAGIC LINK AUTH SCREEN 222 ============================================ */ 223#auth-screen { 224 display: none; 225 min-height: 100vh; 226 flex-direction: column; 227 align-items: center; 228 justify-content: center; 229 background: var(--cream); 230 position: relative; 231} 232 233#auth-screen::before { 234 content: ''; 235 position: absolute; 236 top: 0; left: 0; right: 0; 237 height: 200px; 238 background: var(--plum); 239} 240 241.auth-card { 242 position: relative; 243 background: var(--cream); 244 border: 1px solid var(--gold-light); 245 border-radius: 12px; 246 padding: 50px 50px 44px; 247 max-width: 460px; 248 width: 90%; 249 text-align: center; 250 box-shadow: 0 8px 40px rgba(139, 34, 82, 0.08); 251 margin-top: -40px; 252} 253 254.auth-icon { 255 font-size: 32px; 256 color: var(--gold); 257 margin-bottom: 16px; 258} 259 260.auth-title { 261 font-family: 'Cormorant Garamond', serif; 262 font-size: 26px; 263 color: var(--plum-deep); 264 font-weight: 400; 265 margin-bottom: 8px; 266} 267 268.auth-text { 269 font-family: 'Cormorant Garamond', serif; 270 font-style: italic; 271 font-size: 15px; 272 color: var(--plum); 273 margin-bottom: 24px; 274 line-height: 1.6; 275} 276 277.auth-input { 278 width: 100%; 279 padding: 14px 20px; 280 border: 1px solid var(--gold-light); 281 border-radius: 8px; 282 background: white; 283 font-family: 'Lora', serif; 284 font-size: 14px; 285 color: var(--charcoal); 286 text-align: center; 287 outline: none; 288 transition: border-color 0.3s; 289 margin-bottom: 16px; 290} 291 292.auth-input:focus { 293 border-color: var(--gold); 294 box-shadow: 0 0 0 3px rgba(197, 165, 90, 0.15); 295} 296 297.auth-btn { 298 width: 100%; 299 padding: 14px; 300 background: var(--berry); 301 color: var(--gold); 302 border: 2px solid var(--gold); 303 border-radius: 8px; 304 font-family: 'Marcellus', serif; 305 font-size: 12px; 306 letter-spacing: 3px; 307 text-transform: uppercase; 308 cursor: pointer; 309 transition: all 0.3s; 310} 311 312.auth-btn:hover { 313 background: var(--plum-deep); 314 border-color: var(--gold-light); 315 box-shadow: 0 4px 16px rgba(148, 14, 75, 0.3); 316} 317 318.auth-btn:disabled { 319 opacity: 0.5;
320 cursor: not-allowed; 321} 322 323.auth-message { 324 font-family: 'Cormorant Garamond', serif; 325 font-style: italic; 326 font-size: 14px; 327 color: var(--berry); 328 margin-top: 16px; 329 opacity: 0; 330 transition: opacity 0.3s; 331 line-height: 1.5; 332} 333 334.auth-message.show { opacity: 1; } 335 336.auth-message.success { color: var(--plum); } 337 338/* ============================================ 339 NAME ONBOARDING 340 ============================================ */ 341#name-screen { 342 display: none; 343 min-height: 100vh; 344 flex-direction: column; 345 align-items: center; 346 justify-content: center; 347 background: var(--cream); 348 position: relative; 349} 350 351#name-screen::before { 352 content: ''; 353 position: absolute; 354 top: 0; left: 0; right: 0; 355 height: 200px; 356 background: var(--plum); 357} 358 359/* ============================================ 360 HUB LAYOUT 361 ============================================ */ 362#hub { 363 display: none; 364 min-height: 100vh; 365} 366 367.hub-layout { 368 display: flex; 369 min-height: 100vh; 370} 371 372/* Sidebar */ 373.sidebar { 374 width: var(--sidebar-width); 375 background: white; 376 border-right: 1px solid rgba(197, 165, 90, 0.2); 377 display: flex; 378 flex-direction: column; 379 position: fixed; 380 top: 0; 381 left: 0; 382 bottom: 0; 383 z-index: 100; 384 transition: transform 0.3s ease; 385} 386 387.sidebar-header { 388 padding: 28px 24px 20px; 389 border-bottom: 1px solid rgba(197, 165, 90, 0.15); 390 text-align: center; 391} 392 393.sidebar-logo { 394 width: 40px; 395 height: 40px; 396 margin-bottom: 10px; 397} 398 399.sidebar-brand { 400 font-family: 'Marcellus', serif; 401 font-size: 9px; 402 letter-spacing: 4px; 403 color: var(--gold); 404 text-transform: uppercase; 405 margin-bottom: 4px; 406} 407 408.sidebar-title { 409 font-family: 'Cormorant Garamond', serif; 410 font-size: 18px; 411 color: var(--plum-deep); 412 font-weight: 400; 413} 414 415.sidebar-nav { 416 flex: 1; 417 padding: 20px 0; 418 overflow-y: auto; 419} 420 421.sidebar-link { 422 display: flex; 423 align-items: center; 424 gap: 12px; 425 padding: 14px 24px; 426 font-family: 'Marcellus', serif; 427 font-size: 10px; 428 letter-spacing: 2.5px; 429 color: var(--charcoal); 430 text-transform: uppercase; 431 text-decoration: none; 432 cursor: pointer; 433 transition: all 0.3s; 434 border-left: 3px solid transparent; 435 opacity: 0.6; 436} 437 438.sidebar-link:hover { 439 opacity: 1; 440 background: var(--cream); 441 color: var(--plum); 442} 443 444.sidebar-link.active { 445 opacity: 1; 446 color: var(--berry); 447 border-left-color: var(--berry); 448 background: rgba(148, 14, 75, 0.04); 449} 450 451.sidebar-link-icon { 452 font-size: 16px; 453 width: 20px; 454 text-align: center; 455 flex-shrink: 0; 456} 457 458.sidebar-footer { 459 padding: 20px 24px; 460 border-top: 1px solid rgba(197, 165, 90, 0.15); 461} 462 463.sidebar-user { 464 font-family: 'Cormorant Garamond', serif; 465 font-style: italic; 466 font-size: 15px; 467 color: var(--plum); 468 margin-bottom: 8px; 469 display: flex; 470 align-items: center; 471 gap: 8px; 472} 473 474.sidebar-presence-dot { 475 display: inline-block; 476 width: 8px; 477 height: 8px; 478 border-radius: 50%; 479 background: #4caf50; 480 box-shadow: 0 0 0 0 rgba(76, 175, 80, 0.6); 481 animation: presencePulse 2s ease-in-out infinite; 482} 483 484@keyframes presencePulse { 485 0%, 100% { box-shadow: 0 0 0 0 rgba(76, 175, 80, 0.6); } 486 50% { box-shadow: 0 0 0 6px rgba(76, 175, 80, 0); } 487} 488 489.sidebar-presence-toggle { 490 display: block; 491 background: none; 492 border: 1px solid var(--gold); 493 border-radius: 14px; 494 font-family: 'Marcellus', serif; 495 font-size: 10.5px; 496 letter-spacing: 1.2px; 497 color: var(--berry); 498 font-weight: 600; 499 text-transform: uppercase; 500 cursor: pointer; 501 padding: 7px 14px; 502 margin-bottom: 10px; 503 transition: all 0.3s; 504} 505.sidebar-presence-toggle:hover { 506 border-color: var(--berry); 507 color: var(--berry); 508} 509.sidebar-presence-toggle.is-here { 510 background: var(--berry); 511 color: white; 512 border-color: var(--berry); 513} 514 515/* "Sumir is in the room" banner on home view */ 516.presence-banner { 517 display: inline-flex; 518 align-items: center; 519 gap: 10px; 520 padding: 10px 22px; 521 background: rgba(76, 175, 80, 0.1); 522 border: 1px solid rgba(76, 175, 80, 0.4); 523 border-radius: 24px; 524 margin-bottom: 24px; 525 animation: presenceBannerFade 1s ease; 526} 527.presence-banner-dot { 528 width: 9px; 529 height: 9px; 530 border-radius: 50%; 531 background: #4caf50; 532 box-shadow: 0 0 0 0 rgba(76, 175, 80, 0.6); 533 animation: presencePulse 2s ease-in-out infinite; 534} 535.presence-banner-text { 536 font-family: 'Cormorant Garamond', serif; 537 font-style: italic; 538 font-size: 15px; 539 color: var(--plum-deep); 540} 541@keyframes presenceBannerFade { 542 from { opacity: 0; transform: translateY(-6px); } 543 to { opacity: 1; transform: translateY(0); } 544} 545 546.sidebar-signout { 547 font-family: 'Marcellus', serif; 548 font-size: 9px; 549 letter-spacing: 2px; 550 color: var(--gold); 551 text-transform: uppercase; 552 cursor: pointer; 553 text-decoration: none; 554 transition: color 0.3s; 555 background: none; 556 border: none; 557 padding: 0; 558} 559 560.sidebar-signout:hover { color: var(--berry); } 561 562.sidebar-back { 563 display: block; 564 margin-top: 12px; 565 font-family: 'Marcellus', serif; 566 font-size: 9px; 567 letter-spacing: 2px; 568 color: var(--gold); 569 text-transform: uppercase; 570 text-decoration: none; 571 transition: color 0.3s; 572} 573 574.sidebar-back:hover { color: var(--plum); } 575 576/* Mobile hamburger */ 577.hamburger { 578 display: none; 579 position: fixed; 580 top: 16px; 581 left: 16px; 582 z-index: 200; 583 width: 40px; 584 height: 40px; 585 background: var(--plum); 586 border: 1px solid var(--gold); 587 border-radius: 8px; 588 cursor: pointer; 589 align-items: center; 590 justify-content: center; 591 flex-direction: column; 592 gap: 5px; 593} 594 595.hamburger span { 596 display: block; 597 width: 18px; 598 height: 1.5px; 599 background: var(--gold); 600 transition: all 0.3s; 601} 602 603.sidebar-overlay { 604 display: none; 605 position: fixed; 606 inset: 0; 607 background: rgba(0,0,0,0.3); 608 z-index: 90; 609} 610 611/* Content area */ 612.content-area { 613 flex: 1; 614 margin-left: var(--sidebar-width); 615 min-height: 100vh; 616} 617 618.content-header { 619 background: var(--plum); 620 padding: 32px 40px 28px; 621 position: relative; 622} 623 624.content-header::after {
625 content: ''; 626 position: absolute; 627 bottom: -3px; 628 left: 5%; right: 5%; 629 height: 3px; 630 background: var(--gold); 631} 632 633.content-header-eyebrow { 634 font-family: 'Marcellus', serif; 635 font-size: 10px; 636 letter-spacing: 5px; 637 color: var(--gold); 638 text-transform: uppercase; 639 margin-bottom: 6px; 640} 641 642.content-header-title { 643 font-family: 'Cormorant Garamond', serif; 644 font-size: 30px; 645 color: white; 646 font-weight: 400; 647} 648 649.content-body { 650 max-width: 720px; 651 margin: 0 auto; 652 padding: 40px 30px 60px; 653} 654 655/* ============================================ 656 VIEW CONTAINERS 657 ============================================ */ 658.view { display: none; } 659.view.active { display: block; } 660 661/* ============================================ 662 COMMUNITY FEED 663 ============================================ */ 664/* Post composer (teacher only) */ 665.composer { 666 background: white; 667 border-radius: 14px; 668 border: 1px solid rgba(197, 165, 90, 0.25); 669 margin-bottom: 28px; 670 overflow: hidden; 671} 672 673.composer-trigger { 674 display: flex; 675 align-items: center; 676 gap: 12px; 677 padding: 16px 22px; 678 cursor: pointer; 679 transition: background 0.2s; 680} 681.composer-trigger:hover { background: rgba(197, 165, 90, 0.06); } 682.composer-trigger-avatar { 683 width: 36px; 684 height: 36px; 685 border-radius: 50%; 686 background: var(--plum); 687 color: var(--gold); 688 display: flex; 689 align-items: center; 690 justify-content: center; 691 font-family: 'Marcellus', serif; 692 font-size: 14px; 693 font-weight: 600; 694 flex-shrink: 0; 695} 696.composer-trigger-text { 697 font-family: 'Lora', serif; 698 font-size: 14px; 699 color: #aaa; 700 font-style: italic; 701} 702 703.composer-expanded { 704 display: none; 705 padding: 0 22px 20px; 706 border-top: 1px solid rgba(197, 165, 90, 0.15); 707} 708.composer-expanded.open { display: block; } 709 710.composer-label { 711 font-family: 'Marcellus', serif; 712 font-size: 9px; 713 letter-spacing: 3px; 714 color: var(--gold); 715 text-transform: uppercase; 716 margin-bottom: 14px; 717 padding-top: 18px; 718} 719 720.composer-title-input { 721 width: 100%; 722 padding: 10px 14px; 723 border: 1px solid rgba(197, 165, 90, 0.3); 724 border-radius: 6px; 725 background: var(--cream); 726 font-family: 'Cormorant Garamond', serif; 727 font-size: 20px; 728 color: var(--plum-deep); 729 outline: none; 730 margin-bottom: 10px; 731 transition: border-color 0.3s; 732} 733 734.composer-title-input:focus { border-color: var(--gold); } 735.composer-title-input::placeholder { color: #bbb; } 736 737.composer-textarea { 738 width: 100%; 739 min-height: 120px; 740 padding: 14px; 741 border: 1px solid rgba(197, 165, 90, 0.3); 742 border-radius: 6px; 743 background: var(--cream); 744 font-family: 'Lora', serif; 745 font-size: 13px; 746 line-height: 1.75; 747 color: var(--charcoal); 748 outline: none; 749 resize: vertical; 750 transition: border-color 0.3s; 751} 752 753.composer-textarea:focus { border-color: var(--gold); } 754.composer-textarea::placeholder { color: #bbb; } 755 756.composer-footer { 757 display: flex; 758 justify-content: space-between; 759 align-items: center; 760 margin-top: 14px; 761} 762 763.composer-type { 764 font-family: 'Marcellus', serif; 765 font-size: 10px; 766 letter-spacing: 1px; 767 color: var(--charcoal); 768 padding: 6px 12px; 769 border: 1px solid rgba(197, 165, 90, 0.3); 770 border-radius: 6px; 771 background: white; 772 outline: none; 773 cursor: pointer; 774} 775 776.composer-submit { 777 padding: 10px 28px; 778 background: var(--plum); 779 color: var(--gold); 780 border: none; 781 border-radius: 8px; 782 font-family: 'Marcellus', serif; 783 font-size: 10px; 784 letter-spacing: 2px; 785 text-transform: uppercase; 786 cursor: pointer; 787 transition: all 0.3s; 788} 789 790.composer-submit:hover { 791 background: var(--plum-deep); 792 box-shadow: 0 3px 12px rgba(107, 26, 66, 0.3); 793} 794 795.composer-submit:disabled { opacity: 0.4; cursor: not-allowed; } 796 797/* Voice recorder */ 798#vr-mic-btn.recording { 799 background: var(--gold); 800 color: var(--berry); 801 animation: vrPulse 1.4s ease-in-out infinite; 802} 803@keyframes vrPulse { 804 0%, 100% { box-shadow: 0 0 0 0 rgba(197, 165, 90, 0.6); transform: scale(1); } 805 50% { box-shadow: 0 0 0 16px rgba(197, 165, 90, 0); transform: scale(1.04); } 806} 807#vr-mic-btn:hover { 808 transform: scale(1.05); 809} 810 811/* Inline voice-note player in feed */ 812.voice-note-player { 813 display: flex; 814 align-items: center; 815 gap: 14px; 816 padding: 16px 18px; 817 background: linear-gradient(135deg, var(--cream-warm) 0%, rgba(197, 165, 90, 0.08) 100%); 818 border: 1px solid rgba(197, 165, 90, 0.3); 819 border-left: 4px solid var(--berry); 820 border-radius: 12px; 821 margin: 14px 0; 822} 823.voice-note-icon { 824 width: 40px; height: 40px; 825 border-radius: 50%; 826 background: var(--berry); 827 color: var(--gold-light); 828 display: flex; align-items: center; justify-content: center; 829 font-size: 20px; 830 flex-shrink: 0; 831} 832.voice-note-label { 833 font-family: 'Marcellus', serif; 834 font-size: 9px; 835 letter-spacing: 2px; 836 color: var(--berry); 837 text-transform: uppercase; 838 margin-bottom: 2px; 839} 840.vn-play-btn { 841 width: 42px; height: 42px; 842 border-radius: 50%; 843 background: var(--berry); 844 color: var(--gold-light); 845 border: none; 846 cursor: pointer; 847 display: flex; align-items: center; justify-content: center; 848 font-size: 18px; 849 flex-shrink: 0; 850 transition: transform 0.15s, background 0.15s; 851} 852.vn-play-btn:hover { transform: scale(1.08); background: var(--plum-deep); } 853.vn-controls { flex: 1; display: flex; flex-direction: column; gap: 4px; } 854.vn-progress-wrap { 855 width: 100%; 856 height: 20px; 857 background: transparent; 858 cursor: pointer; 859 position: relative; 860 display: flex; 861 align-items: center; 862} 863.vn-progress-track { 864 width: 100%; 865 height: 5px; 866 background: rgba(197,165,90,0.2); 867 border-radius: 3px; 868 position: relative; 869} 870.vn-progress-bar { 871 height: 100%; 872 background: var(--berry); 873 border-radius: 3px; 874 width: 0%; 875 position: relative; 876} 877.vn-progress-bar::after {
878 content: ''; 879 position: absolute; 880 right: -6px; 881 top: 50%; 882 transform: translateY(-50%); 883 width: 12px; 884 height: 12px; 885 background: var(--berry); 886 border-radius: 50%; 887 box-shadow: 0 1px 4px rgba(0,0,0,0.2); 888} 889.vn-time-row { 890 display: flex; 891 justify-content: space-between; 892 align-items: center; 893} 894.vn-elapsed, .vn-remaining { 895 font-family: 'Marcellus', serif; 896 font-size: 10px; 897 letter-spacing: 1px; 898 color: var(--plum); 899 opacity: 0.7; 900} 901 902/* Feed posts */ 903.feed-empty { 904 text-align: center; 905 padding: 60px 20px; 906} 907 908.feed-empty-icon { 909 font-size: 36px; 910 color: var(--gold-light); 911 margin-bottom: 16px; 912} 913 914.feed-empty-text { 915 font-family: 'Cormorant Garamond', serif; 916 font-style: italic; 917 font-size: 18px; 918 color: var(--plum); 919 opacity: 0.6; 920} 921 922.post-card { 923 background: white; 924 border-radius: 14px; 925 border: 1px solid rgba(197, 165, 90, 0.2); 926 border-left: 4px solid var(--berry); 927 padding: 32px 28px 24px; 928 margin-bottom: 28px; 929 animation: fadeInUp 0.6s ease; 930} 931 932@keyframes fadeInUp { 933 from { opacity: 0; transform: translateY(12px); } 934 to { opacity: 1; transform: translateY(0); } 935} 936 937.post-type-label { 938 font-family: 'Marcellus', serif; 939 font-size: 11px; 940 letter-spacing: 2px; 941 color: var(--berry); 942 text-transform: uppercase; 943 margin-bottom: 10px; 944 font-weight: 600; 945 opacity: 0.95; 946} 947 948.post-title { 949 font-family: 'Cormorant Garamond', serif; 950 font-size: 24px; 951 color: var(--plum-deep); 952 font-weight: 400; 953 margin-bottom: 14px; 954 line-height: 1.3; 955} 956 957.post-body { 958 font-family: 'Lora', serif; 959 font-size: 13px; 960 line-height: 1.85; 961 color: var(--charcoal); 962 opacity: 0.88; 963 margin-bottom: 20px; 964 white-space: pre-wrap; 965} 966 967.post-meta { 968 display: flex; 969 justify-content: space-between; 970 align-items: center; 971 padding-top: 16px; 972 border-top: 1px solid rgba(197, 165, 90, 0.12); 973} 974 975.post-author { 976 display: flex; 977 align-items: center; 978 gap: 8px; 979} 980 981.post-author-name { 982 font-family: 'Marcellus', serif; 983 font-size: 11px; 984 letter-spacing: 1px; 985 color: var(--berry); 986} 987 988.post-date { 989 font-family: 'Marcellus', serif; 990 font-size: 11px; 991 letter-spacing: 1px; 992 color: var(--berry); 993 opacity: 0.85; 994} 995 996.heart-btn { 997 display: flex; 998 align-items: center; 999 gap: 6px; 1000 background: none; 1001 border: none; 1002 cursor: pointer; 1003 padding: 6px 10px; 1004 border-radius: 20px; 1005 transition: all 0.3s; 1006} 1007 1008.heart-btn:hover { 1009 background: rgba(197, 165, 90, 0.1); 1010} 1011 1012.heart-icon { 1013 font-size: 19px; 1014 color: var(--berry); 1015 opacity: 0.55; 1016 transition: all 0.3s; 1017} 1018 1019.heart-btn:hover .heart-icon { 1020 opacity: 1; 1021} 1022 1023.heart-btn.hearted .heart-icon { 1024 color: var(--berry); 1025 opacity: 1; 1026} 1027 1028.heart-count { 1029 font-family: 'Marcellus', serif; 1030 font-size: 12px; 1031 color: var(--berry); 1032 font-weight: 600; 1033 min-width: 12px; 1034} 1035 1036@keyframes heartPulse { 1037 0% { transform: scale(1); } 1038 50% { transform: scale(1.3); } 1039 100% { transform: scale(1); } 1040} 1041 1042.heart-btn.pulse .heart-icon { 1043 animation: heartPulse 0.3s ease; 1044} 1045 1046/* Sister-to-sister emoji reactions on posts/comments */ 1047.sister-reactions-row { 1048 display: flex; 1049 flex-wrap: wrap; 1050 gap: 6px; 1051 align-items: center; 1052 margin: 12px 0 0; 1053} 1054.sister-reaction-chip { 1055 display: inline-flex; 1056 align-items: center; 1057 gap: 4px; 1058 padding: 3px 9px; 1059 background: rgba(197, 165, 90, 0.1); 1060 border: 1px solid rgba(197, 165, 90, 0.25); 1061 border-radius: 14px; 1062 font-size: 13px; 1063 cursor: pointer; 1064 transition: all 0.2s; 1065} 1066.sister-reaction-chip:hover { 1067 background: rgba(197, 165, 90, 0.2); 1068} 1069.sister-reaction-chip.mine { 1070 background: rgba(148, 14, 75, 0.12); 1071 border-color: rgba(148, 14, 75, 0.4); 1072} 1073.sister-reaction-count { 1074 font-family: 'Marcellus', serif; 1075 font-size: 9px; 1076 color: var(--berry); 1077 letter-spacing: 1px; 1078} 1079.sister-reaction-picker { 1080 display: inline-flex; 1081 gap: 2px; 1082 padding: 2px 4px; 1083 background: white; 1084 border: 1px solid rgba(197, 165, 90, 0.3); 1085 border-radius: 14px; 1086 opacity: 0.6; 1087 transition: opacity 0.2s; 1088} 1089.sister-reaction-picker:hover { 1090 opacity: 1; 1091} 1092.sister-reaction-add { 1093 background: none; 1094 border: none; 1095 padding: 3px 6px; 1096 font-size: 13px; 1097 cursor: pointer; 1098 border-radius: 10px; 1099 transition: all 0.2s; 1100} 1101.sister-reaction-add:hover { 1102 background: rgba(197, 165, 90, 0.15); 1103 transform: scale(1.2); 1104} 1105 1106/* Post delete */ 1107.post-delete { 1108 background: none; 1109 border: none; 1110 font-size: 14px; 1111 color: var(--gold-light); 1112 cursor: pointer; 1113 opacity: 0.4; 1114 transition: all 0.3s; 1115 padding: 4px 8px; 1116} 1117 1118.post-delete:hover { 1119 opacity: 1; 1120 color: var(--berry); 1121} 1122 1123/* Reflections */ 1124.reflections-section { 1125 margin-top: 16px; 1126 padding-top: 16px; 1127 border-top: 1px solid rgba(197, 165, 90, 0.1); 1128} 1129 1130.reflections-toggle { 1131 font-family: 'Marcellus', serif; 1132 font-size: 11px; 1133 letter-spacing: 1.5px; 1134 color: var(--berry); 1135 font-weight: 600; 1136 text-transform: uppercase; 1137 cursor: pointer; 1138 background: none; 1139 border: none; 1140 padding: 6px 0; 1141 transition: color 0.3s; 1142 opacity: 0.85; 1143} 1144.reflections-toggle:hover { opacity: 1; } 1145 1146/* "Show more comments" button for long threads */ 1147.show-more-comments { 1148 display: block; 1149 width: 100%; 1150 background: var(--cream-warm); 1151 border: 1px dashed rgba(197, 165, 90, 0.4); 1152 color: var(--plum); 1153 font-family: 'Marcellus', serif; 1154 font-size: 10px; 1155 letter-spacing: 1.5px; 1156 text-transform: uppercase; 1157 padding: 10px 16px; 1158 margin-bottom: 12px; 1159 border-radius: 8px; 1160 cursor: pointer; 1161 transition: all 0.3s; 1162} 1163.show-more-comments:hover { 1164 background: var(--cream); 1165 border-color: var(--gold); 1166 color: var(--berry); 1167} 1168 1169.reply-btn { 1170 font-family: 'Marcellus', serif; 1171 font-size: 11px; 1172 letter-spacing: 1.5px; 1173 color: var(--plum-deep); 1174 font-weight: 600; 1175 text-transform: uppercase; 1176 cursor: pointer; 1177 background: none; 1178 border: none; 1179 padding: 6px 0; 1180 margin-left: 16px; 1181 transition: color 0.3s; 1182 opacity: 0.85; 1183} 1184.reply-btn:hover { color: var(--berry); opacity: 1; } 1185 1186.reflections-bar { 1187 display: flex; 1188 align-items: center; 1189} 1190 1191.reflections-list { 1192 margin-top: 14px; 1193} 1194 1195.reflection-card { 1196 background: var(--cream-warm); 1197 border-radius: 10px; 1198 border: 1px solid rgba(197, 165, 90, 0.12); 1199 padding: 18px 20px 14px; 1200 margin-bottom: 10px; 1201 animation: fadeInUp 0.4s ease; 1202} 1203 1204.reflection-body { 1205 font-family: 'Lora', serif; 1206 font-size: 13px; 1207 line-height: 1.75; 1208 color: var(--charcoal); 1209 opacity: 0.85; 1210 margin-bottom: 10px; 1211 white-space: pre-wrap; 1212} 1213 1214.reflection-meta { 1215 display: flex; 1216 justify-content: space-between; 1217 align-items: center; 1218} 1219 1220.reflection-author { 1221 font-family: 'Cormorant Garamond', serif; 1222 font-style: italic; 1223 font-size: 14px; 1224 color: var(--plum); 1225} 1226 1227.reflection-date { 1228 font-family: 'Marcellus', serif; 1229 font-size: 8px; 1230 letter-spacing: 1px; 1231 color: var(--charcoal); 1232 opacity: 0.35; 1233} 1234 1235.reflection-composer { 1236 margin-top: 14px; 1237} 1238 1239.reflection-textarea { 1240 width: 100%; 1241 min-height: 70px; 1242 padding: 12px; 1243 border: 1px solid rgba(197, 165, 90, 0.25); 1244 border-radius: 8px; 1245 background: white; 1246 font-family: 'Lora', serif; 1247 font-size: 12px; 1248 line-height: 1.65; 1249 color: var(--charcoal); 1250 outline: none; 1251 resize: vertical; 1252 transition: border-color 0.3s; 1253} 1254 1255.reflection-textarea:focus { border-color: var(--gold); } 1256.reflection-textarea::placeholder { color: #bbb; } 1257 1258.reflection-submit { 1259 margin-top: 8px; 1260 padding: 8px 20px; 1261 background: var(--cream-warm); 1262 color: var(--plum); 1263 border: 1px solid var(--gold-light); 1264 border-radius: 6px; 1265 font-family: 'Marcellus', serif; 1266 font-size: 9px; 1267 letter-spacing: 2px; 1268 text-transform: uppercase; 1269 cursor: pointer; 1270 transition: all 0.3s; 1271} 1272 1273.reflection-submit:hover { 1274 background: var(--gold); 1275 color: white; 1276 border-color: var(--gold); 1277} 1278
1279/* Diamond divider */ 1280.feed-divider { 1281 display: flex; 1282 align-items: center; 1283 justify-content: center; 1284 gap: 14px; 1285 padding: 4px 0 24px; 1286 max-width: 200px; 1287 margin: 0 auto; 1288} 1289 1290.feed-divider::before, 1291.feed-divider::after { 1292 content: ''; 1293 flex: 1; 1294 height: 1px; 1295 background: var(--gold-light); 1296} 1297 1298.feed-divider span { color: var(--gold); font-size: 8px; } 1299 1300/* ============================================ 1301 ABOUT VIEW 1302 ============================================ */ 1303.pillar-card { 1304 background: white; 1305 border-radius: 14px; 1306 border: 1px solid rgba(197, 165, 90, 0.2); 1307 padding: 36px 32px 32px; 1308 margin-bottom: 20px; 1309} 1310 1311.pillar-number { 1312 font-family: 'Marcellus', serif; 1313 font-size: 9px; 1314 letter-spacing: 3px; 1315 color: var(--gold); 1316 text-transform: uppercase; 1317 margin-bottom: 8px; 1318} 1319 1320.pillar-title { 1321 font-family: 'Cormorant Garamond', serif; 1322 font-size: 22px; 1323 color: var(--plum-deep); 1324 font-weight: 400; 1325 margin-bottom: 14px; 1326} 1327 1328.pillar-body { 1329 font-family: 'Lora', serif; 1330 font-size: 13px; 1331 line-height: 1.75; 1332 color: var(--charcoal); 1333 opacity: 0.85; 1334 margin-bottom: 14px; 1335} 1336 1337.pillar-movement { 1338 font-family: 'Cormorant Garamond', serif; 1339 font-style: italic; 1340 font-size: 14px; 1341 color: var(--berry); 1342 padding-top: 10px; 1343 border-top: 1px solid rgba(197, 165, 90, 0.15); 1344} 1345 1346.about-welcome { 1347 font-family: 'Cormorant Garamond', serif; 1348 font-style: italic; 1349 font-size: 18px; 1350 line-height: 1.7; 1351 color: var(--plum); 1352 text-align: center; 1353 margin-bottom: 32px; 1354} 1355 1356.about-divider { 1357 display: flex; 1358 align-items: center; 1359 justify-content: center; 1360 gap: 14px; 1361 padding: 10px 0 30px; 1362 max-width: 300px; 1363 margin: 0 auto; 1364} 1365 1366.about-divider::before, 1367.about-divider::after { 1368 content: ''; 1369 flex: 1; 1370 height: 1px; 1371 background: var(--gold-light); 1372} 1373 1374.about-divider span { color: var(--gold); font-size: 10px; } 1375 1376/* Structure card */ 1377.structure-card { 1378 background: white; 1379 border-radius: 14px; 1380 border: 1px solid rgba(197, 165, 90, 0.2); 1381 padding: 40px 32px; 1382 text-align: center; 1383 margin-bottom: 20px; 1384} 1385 1386.structure-heading { 1387 font-family: 'Marcellus', serif; 1388 font-size: 10px; 1389 letter-spacing: 4px; 1390 color: var(--gold); 1391 text-transform: uppercase; 1392 margin-bottom: 6px; 1393} 1394 1395.structure-title { 1396 font-family: 'Cormorant Garamond', serif; 1397 font-size: 24px; 1398 color: var(--plum-deep); 1399 font-weight: 400; 1400 margin-bottom: 20px; 1401} 1402 1403.structure-item { 1404 display: flex; 1405 gap: 14px; 1406 margin-bottom: 20px; 1407 align-items: flex-start; 1408 text-align: left; 1409 max-width: 520px; 1410 margin-left: auto; 1411 margin-right: auto; 1412} 1413 1414.structure-icon { 1415 color: var(--gold); 1416 font-size: 14px; 1417 margin-top: 2px; 1418 flex-shrink: 0; 1419} 1420 1421.structure-item-title { 1422 font-family: 'Cormorant Garamond', serif; 1423 font-size: 17px; 1424 color: var(--plum-deep); 1425 margin-bottom: 4px; 1426} 1427 1428.structure-item-text { 1429 font-family: 'Lora', serif; 1430 font-size: 12px; 1431 line-height: 1.65; 1432 color: var(--charcoal); 1433 opacity: 0.8; 1434} 1435 1436/* Leadership card */ 1437.leadership-card { 1438 background: var(--plum-deep); 1439 border-radius: 14px; 1440 padding: 44px 36px; 1441 position: relative; 1442 overflow: hidden; 1443 margin-bottom: 20px; 1444} 1445 1446.leadership-card::before { 1447 content: ''; 1448 position: absolute; 1449 top: 0; left: 0; right: 0; 1450 height: 3px; 1451 background: var(--gold); 1452} 1453 1454.leadership-eyebrow { 1455 font-family: 'Marcellus', serif; 1456 font-size: 9px; 1457 letter-spacing: 4px; 1458 color: var(--gold); 1459 text-transform: uppercase; 1460 margin-bottom: 10px; 1461 text-align: center; 1462} 1463 1464.leadership-title { 1465 font-family: 'Cormorant Garamond', serif; 1466 font-size: 24px; 1467 color: white; 1468 font-weight: 400; 1469 text-align: center; 1470 margin-bottom: 20px; 1471} 1472 1473.leadership-body { 1474 font-family: 'Lora', serif; 1475 font-size: 13px; 1476 line-height: 1.8; 1477 color: rgba(255,255,255,0.85); 1478 margin-bottom: 16px; 1479} 1480 1481.leadership-emphasis { 1482 font-family: 'Cormorant Garamond', serif; 1483 font-style: italic; 1484 font-size: 15px; 1485 line-height: 1.7; 1486 color: var(--gold-light); 1487 text-align: center; 1488 padding-top: 16px; 1489 border-top: 1px solid rgba(197, 165, 90, 0.25); 1490} 1491 1492/* ============================================ 1493 JOURNAL VIEW 1494 ============================================ */ 1495/* Thoughtful Inquiries card */ 1496.inquiry-banner { 1497 background: white; 1498 border-radius: 14px; 1499 border: 1px solid rgba(197, 165, 90, 0.25); 1500 margin-bottom: 24px; 1501 overflow: hidden; 1502 box-shadow: 0 2px 16px rgba(139, 34, 82, 0.04); 1503} 1504.inquiry-banner-header { 1505 display: flex; 1506 align-items: center; 1507 justify-content: space-between; 1508 padding: 20px 24px; 1509 cursor: pointer; 1510 transition: background 0.2s; 1511 border-left: 4px solid var(--berry); 1512} 1513.inquiry-banner-header:hover { background: var(--cream-warm); } 1514.inquiry-banner-title { 1515 font-family: 'Cormorant Garamond', serif; 1516 font-size: 20px; 1517 color: var(--berry); 1518 font-weight: 600; 1519} 1520.inquiry-banner-subtitle { 1521 font-family: 'Lora', serif; 1522 font-size: 11px; 1523 color: var(--charcoal); 1524 opacity: 0.6; 1525 margin-top: 2px; 1526} 1527.inquiry-banner-arrow { 1528 font-size: 16px; 1529 color: var(--gold); 1530 transition: transform 0.3s; 1531} 1532.inquiry-banner.open .inquiry-banner-arrow { transform: rotate(180deg); } 1533.inquiry-banner-body { 1534 display: none; 1535 padding: 0 28px 28px; 1536 border-top: 1px solid rgba(197, 165, 90, 0.12); 1537} 1538.inquiry-banner.open .inquiry-banner-body { display: block; } 1539.inquiry-intro { 1540 font-family: 'Cormorant Garamond', serif; 1541 font-style: italic; 1542 font-size: 16px; 1543 line-height: 1.7; 1544 color: var(--plum); 1545 text-align: center; 1546 padding: 24px 10px 20px; 1547} 1548.inquiry-section { 1549 margin-bottom: 24px; 1550} 1551.inquiry-section-num { 1552 font-family: 'Marcellus', serif; 1553 font-size: 9px; 1554 letter-spacing: 3px; 1555 text-transform: uppercase; 1556 color: var(--gold); 1557 margin-bottom: 8px; 1558} 1559.inquiry-section-q { 1560 font-family: 'Cormorant Garamond', serif; 1561 font-size: 19px; 1562 line-height: 1.5; 1563 color: var(--berry); 1564 font-weight: 600; 1565 margin-bottom: 14px; 1566} 1567.inquiry-cat-title { 1568 font-family: 'Marcellus', serif; 1569 font-size: 10px; 1570 letter-spacing: 2px; 1571 text-transform: uppercase; 1572 color: var(--berry); 1573 font-weight: 700; 1574 margin: 16px 0 8px; 1575} 1576.inquiry-input { 1577 width: 100%; 1578 border: none; 1579 border-bottom: 1px solid rgba(197, 165, 90, 0.3); 1580 background: transparent; 1581 font-family: 'Lora', serif; 1582 font-size: 13px; 1583 color: var(--charcoal); 1584 padding: 8px 4px; 1585 margin-bottom: 6px; 1586 outline: none; 1587 transition: border-color 0.2s; 1588} 1589.inquiry-input:focus { border-bottom-color: var(--berry); } 1590.inquiry-input::placeholder { color: var(--gold-light); font-style: italic; } 1591.inquiry-line-label { 1592 font-family: 'Marcellus', serif; 1593 font-size: 10px; 1594 color: var(--gold); 1595 margin-right: 8px; 1596 flex-shrink: 0; 1597} 1598.inquiry-line-row { 1599 display: flex; 1600 align-items: flex-end; 1601 gap: 4px; 1602} 1603.inquiry-save-row { 1604 display: flex; 1605 align-items: center; 1606 justify-content: space-between; 1607 margin-top: 20px; 1608 padding-top: 16px; 1609 border-top: 1px solid rgba(197, 165, 90, 0.12); 1610} 1611.inquiry-save-btn { 1612 font-family: 'Marcellus', serif; 1613 font-size: 10px; 1614 letter-spacing: 2px; 1615 text-transform: uppercase; 1616 color: var(--gold); 1617 background: var(--berry); 1618 border: 1.5px solid var(--gold); 1619 border-radius: 8px; 1620 padding: 10px 24px; 1621 cursor: pointer; 1622 transition: all 0.3s; 1623} 1624.inquiry-save-btn:hover { background: var(--plum-deep); } 1625.inquiry-saved-msg {
1626 font-family: 'Lora', serif; 1627 font-size: 11px; 1628 font-style: italic; 1629 color: var(--gold); 1630 opacity: 0; 1631 transition: opacity 0.3s; 1632} 1633.inquiry-saved-msg.show { opacity: 1; } 1634.inquiry-divider { 1635 display: flex; 1636 align-items: center; 1637 justify-content: center; 1638 gap: 10px; 1639 padding: 8px 0 16px; 1640 max-width: 200px; 1641 margin: 0 auto; 1642} 1643.inquiry-divider::before, .inquiry-divider::after { 1644 content: ''; 1645 flex: 1; 1646 height: 1px; 1647 background: var(--gold-light); 1648} 1649.inquiry-divider span { color: var(--gold); font-size: 8px; } 1650 1651.journal-prompt-card { 1652 background: white; 1653 border-radius: 14px; 1654 border: 1px solid rgba(197, 165, 90, 0.2); 1655 border-left: 4px solid var(--gold); 1656 padding: 28px 28px 24px; 1657 margin-bottom: 20px; 1658} 1659 1660.journal-prompt-label { 1661 font-family: 'Marcellus', serif; 1662 font-size: 9px; 1663 letter-spacing: 3px; 1664 color: var(--gold); 1665 text-transform: uppercase; 1666 margin-bottom: 10px; 1667} 1668 1669.journal-prompt-text { 1670 font-family: 'Cormorant Garamond', serif; 1671 font-style: italic; 1672 font-size: 18px; 1673 color: var(--plum); 1674 line-height: 1.6; 1675} 1676 1677.journal-entry-card { 1678 background: var(--cream-warm); 1679 border-radius: 10px; 1680 border: 1px solid rgba(197, 165, 90, 0.12); 1681 padding: 22px 24px; 1682 margin-bottom: 14px; 1683 cursor: pointer; 1684 transition: border-color 0.3s, box-shadow 0.3s; 1685} 1686.journal-entry-card:hover { 1687 border-color: rgba(197, 165, 90, 0.35); 1688 box-shadow: 0 2px 12px rgba(197, 165, 90, 0.08); 1689} 1690.journal-entry-card.expanded { 1691 cursor: default; 1692 border-color: var(--gold-light); 1693} 1694 1695.journal-entry-date { 1696 font-family: 'Marcellus', serif; 1697 font-size: 9px; 1698 letter-spacing: 2px; 1699 color: var(--berry); 1700 text-transform: uppercase; 1701 margin-bottom: 8px; 1702} 1703 1704.journal-entry-body { 1705 font-family: 'Lora', serif; 1706 font-size: 13px; 1707 line-height: 1.75; 1708 color: var(--charcoal); 1709 opacity: 0.85; 1710 white-space: pre-wrap; 1711} 1712.journal-entry-body.truncated { 1713 display: -webkit-box; 1714 -webkit-line-clamp: 2; 1715 -webkit-box-orient: vertical; 1716 overflow: hidden; 1717} 1718.journal-entry-actions { 1719 display: none; 1720 margin-top: 14px; 1721 padding-top: 12px; 1722 border-top: 1px solid rgba(197, 165, 90, 0.15); 1723 gap: 8px; 1724} 1725.journal-entry-card.expanded .journal-entry-actions { 1726 display: flex; 1727} 1728.journal-entry-card.expanded .journal-entry-body.truncated { 1729 display: block; 1730 -webkit-line-clamp: unset; 1731 overflow: visible; 1732} 1733.journal-entry-action-btn { 1734 padding: 5px 14px; 1735 border-radius: 16px; 1736 font-family: 'Marcellus', serif; 1737 font-size: 9px; 1738 letter-spacing: 1.5px; 1739 text-transform: uppercase; 1740 cursor: pointer; 1741 border: 1px solid; 1742 transition: all 0.3s; 1743} 1744.journal-entry-action-btn.edit-btn { 1745 background: none; 1746 border-color: var(--gold); 1747 color: var(--gold); 1748} 1749.journal-entry-action-btn.edit-btn:hover { 1750 background: var(--gold); 1751 color: white; 1752} 1753.journal-entry-action-btn.delete-btn { 1754 background: none; 1755 border-color: rgba(148, 14, 75, 0.3); 1756 color: var(--berry); 1757} 1758.journal-entry-action-btn.delete-btn:hover { 1759 background: var(--berry); 1760 color: white; 1761} 1762.journal-entry-action-btn.vis-btn { 1763 background: none; 1764 border-color: rgba(148, 14, 75, 0.2); 1765 color: var(--plum); 1766} 1767.journal-entry-action-btn.vis-btn:hover { 1768 background: var(--plum); 1769 color: white; 1770} 1771.journal-entry-collapse-hint { 1772 display: none; 1773 margin-top: 10px; 1774 font-family: 'Marcellus', serif; 1775 font-size: 8px; 1776 letter-spacing: 1.5px; 1777 text-transform: uppercase; 1778 color: var(--gold); 1779 text-align: right; 1780} 1781.journal-entry-card.expanded .journal-entry-collapse-hint { 1782 display: block; 1783} 1784 1785/* Journal thread (reactions + replies) */ 1786.journal-thread { 1787 display: none; 1788 margin-top: 14px; 1789 padding-top: 14px; 1790 border-top: 1px solid rgba(197, 165, 90, 0.18); 1791} 1792.journal-entry-card.expanded .journal-thread { 1793 display: block; 1794} 1795 1796.j-reactions-row { 1797 display: flex; 1798 flex-wrap: wrap; 1799 gap: 6px; 1800 align-items: center; 1801 margin-bottom: 12px; 1802} 1803.j-reaction-chip { 1804 display: inline-flex; 1805 align-items: center; 1806 gap: 4px; 1807 padding: 3px 9px; 1808 background: rgba(197, 165, 90, 0.1); 1809 border: 1px solid rgba(197, 165, 90, 0.25); 1810 border-radius: 14px; 1811 font-size: 13px; 1812 cursor: pointer; 1813 transition: all 0.2s; 1814} 1815.j-reaction-chip:hover { 1816 background: rgba(197, 165, 90, 0.2); 1817} 1818.j-reaction-chip.mine { 1819 background: rgba(148, 14, 75, 0.12); 1820 border-color: rgba(148, 14, 75, 0.4); 1821} 1822.j-reaction-count { 1823 font-family: 'Marcellus', serif; 1824 font-size: 9px; 1825 color: var(--berry); 1826 letter-spacing: 1px; 1827} 1828.j-emoji-picker { 1829 display: inline-flex; 1830 gap: 2px; 1831 padding: 2px 4px; 1832 background: white; 1833 border: 1px solid rgba(197, 165, 90, 0.3); 1834 border-radius: 14px; 1835} 1836.j-emoji-btn { 1837 background: none; 1838 border: none; 1839 padding: 3px 6px; 1840 font-size: 14px; 1841 cursor: pointer; 1842 border-radius: 10px; 1843 transition: all 0.2s; 1844} 1845.j-emoji-btn:hover { 1846 background: rgba(197, 165, 90, 0.15); 1847 transform: scale(1.2); 1848} 1849 1850.j-replies-list { 1851 margin: 12px 0; 1852} 1853.j-reply { 1854 background: white; 1855 border-radius: 8px; 1856 padding: 12px 14px; 1857 margin-bottom: 8px; 1858 border-left: 2px solid var(--gold); 1859} 1860.j-reply-author { 1861 font-family: 'Marcellus', serif; 1862 font-size: 9px; 1863 letter-spacing: 1.5px; 1864 color: var(--berry); 1865 text-transform: uppercase; 1866 margin-bottom: 4px; 1867} 1868.j-reply-body { 1869 font-family: 'Lora', serif; 1870 font-size: 12.5px; 1871 line-height: 1.65; 1872 color: var(--charcoal); 1873 opacity: 0.9; 1874 white-space: pre-wrap; 1875} 1876.j-reply-date { 1877 font-family: 'Marcellus', serif; 1878 font-size: 8px; 1879 letter-spacing: 1px; 1880 color: var(--gold); 1881 margin-top: 6px; 1882 opacity: 0.7; 1883} 1884.j-reply-delete { 1885 background: none; 1886 border: none; 1887 color: var(--berry); 1888 opacity: 0.4; 1889 font-size: 12px; 1890 cursor: pointer; 1891 float: right; 1892 padding: 0 4px; 1893} 1894.j-reply-delete:hover { 1895 opacity: 1; 1896} 1897 1898.j-reply-form { 1899 display: flex; 1900 gap: 8px; 1901 margin-top: 10px; 1902} 1903.j-reply-input { 1904 flex: 1; 1905 padding: 8px 12px; 1906 border: 1px solid rgba(197, 165, 90, 0.3); 1907 border-radius: 18px; 1908 background: white; 1909 font-family: 'Lora', serif; 1910 font-size: 12.5px; 1911 color: var(--charcoal); 1912 outline: none; 1913 transition: border-color 0.3s; 1914} 1915.j-reply-input:focus { border-color: var(--gold); } 1916.j-reply-send { 1917 padding: 6px 16px; 1918 background: var(--plum); 1919 color: var(--gold); 1920 border: none; 1921 border-radius: 18px; 1922 font-family: 'Marcellus', serif; 1923 font-size: 9px; 1924 letter-spacing: 1.5px; 1925 text-transform: uppercase; 1926 cursor: pointer; 1927 transition: all 0.3s; 1928} 1929.j-reply-send:hover { background: var(--plum-deep); } 1930 1931.journal-compose { 1932 background: white; 1933 border-radius: 14px; 1934 border: 1px solid rgba(197, 165, 90, 0.25); 1935 padding: 24px; 1936 margin-bottom: 24px; 1937} 1938 1939.journal-compose-label { 1940 font-family: 'Marcellus', serif; 1941 font-size: 9px; 1942 letter-spacing: 3px; 1943 color: var(--gold); 1944 text-transform: uppercase; 1945 margin-bottom: 12px; 1946} 1947 1948.journal-textarea { 1949 width: 100%; 1950 min-height: 140px; 1951 padding: 14px; 1952 border: 1px solid rgba(197, 165, 90, 0.3); 1953 border-radius: 6px; 1954 background: var(--cream); 1955 font-family: 'Lora', serif; 1956 font-size: 13px; 1957 line-height: 1.75; 1958 color: var(--charcoal); 1959 outline: none; 1960 resize: vertical; 1961 transition: border-color 0.3s; 1962} 1963 1964.journal-textarea:focus { border-color: var(--gold); } 1965 1966.journal-visibility-toggle { 1967 display: flex; 1968 gap: 4px; 1969} 1970.journal-vis-btn { 1971 padding: 5px 12px; 1972 background: none; 1973 border: 1px solid rgba(197, 165, 90, 0.25); 1974 border-radius: 16px; 1975 font-family: 'Marcellus', serif; 1976 font-size: 9px; 1977 letter-spacing: 1px; 1978 text-transform: uppercase; 1979 color: var(--plum-light); 1980 cursor: pointer; 1981 transition: all 0.3s; 1982} 1983.journal-vis-btn:hover { border-color: var(--gold); } 1984.journal-vis-btn.active { 1985 background: var(--gold); 1986 border-color: var(--gold); 1987 color: white; 1988}
1989.journal-entry-visibility { 1990 font-family: 'Marcellus', serif; 1991 font-size: 8px; 1992 letter-spacing: 1.5px; 1993 text-transform: uppercase; 1994 padding: 2px 8px; 1995 border-radius: 10px; 1996 display: inline-block; 1997 margin-left: 8px; 1998} 1999.journal-entry-visibility.private { 2000 background: rgba(197, 165, 90, 0.12); 2001 color: var(--gold); 2002} 2003.journal-entry-visibility.public { 2004 background: rgba(148, 14, 75, 0.1); 2005 color: var(--berry); 2006} 2007 2008.journal-save { 2009 margin-top: 10px; 2010 padding: 10px 24px; 2011 background: var(--plum); 2012 color: var(--gold); 2013 border: none; 2014 border-radius: 8px; 2015 font-family: 'Marcellus', serif; 2016 font-size: 10px; 2017 letter-spacing: 2px; 2018 text-transform: uppercase; 2019 cursor: pointer; 2020 transition: all 0.3s; 2021} 2022 2023.journal-save:hover { 2024 background: var(--plum-deep); 2025} 2026 2027.journal-save-draft { 2028 background: white; 2029 color: var(--plum); 2030 border: 1px solid var(--gold); 2031} 2032.journal-save-draft:hover { 2033 background: var(--cream-warm); 2034 color: var(--berry); 2035} 2036 2037/* Draft badge on entry cards */ 2038.journal-draft-badge { 2039 display: inline-block; 2040 font-family: 'Marcellus', serif; 2041 font-size: 8px; 2042 letter-spacing: 1.5px; 2043 text-transform: uppercase; 2044 padding: 2px 8px; 2045 border-radius: 10px; 2046 background: rgba(197, 165, 90, 0.15); 2047 border: 1px dashed var(--gold); 2048 color: var(--plum); 2049 margin-left: 8px; 2050} 2051 2052/* Bookmark / favorite button (used in library) */ 2053.bookmark-btn { 2054 background: none; 2055 border: none; 2056 cursor: pointer; 2057 font-size: 22px; 2058 color: var(--gold-light); 2059 padding: 4px 8px; 2060 transition: all 0.2s; 2061} 2062.bookmark-btn:hover { 2063 color: var(--berry); 2064 transform: scale(1.15); 2065} 2066.bookmark-btn.bookmarked { 2067 color: var(--berry); 2068} 2069 2070/* ============================================ 2071 MEDIA UPLOAD & EDIT 2072 ============================================ */ 2073.composer-media-row { 2074 display: flex; 2075 align-items: center; 2076 gap: 8px; 2077 margin-top: 8px; 2078} 2079.media-upload-btn { 2080 display: inline-flex; 2081 align-items: center; 2082 gap: 5px; 2083 padding: 6px 14px; 2084 background: none; 2085 border: 1px solid rgba(197, 165, 90, 0.3); 2086 border-radius: 6px; 2087 font-family: 'Marcellus', serif; 2088 font-size: 9px; 2089 letter-spacing: 1.5px; 2090 text-transform: uppercase; 2091 color: var(--plum-light); 2092 cursor: pointer; 2093 transition: all 0.3s; 2094} 2095.media-upload-btn:hover { border-color: var(--gold); color: var(--plum); } 2096.media-upload-btn .upload-icon { font-size: 14px; } 2097.media-preview { 2098 margin-top: 10px; 2099 position: relative; 2100 display: inline-block; 2101} 2102.media-preview img, .media-preview video { 2103 max-width: 100%; 2104 max-height: 200px; 2105 border-radius: 8px; 2106 border: 1px solid rgba(197, 165, 90, 0.2); 2107} 2108.media-preview-remove { 2109 position: absolute; 2110 top: 4px; 2111 right: 4px; 2112 background: rgba(0,0,0,0.6); 2113 color: white; 2114 border: none; 2115 border-radius: 50%; 2116 width: 24px; 2117 height: 24px; 2118 font-size: 14px; 2119 cursor: pointer; 2120 display: flex; 2121 align-items: center; 2122 justify-content: center; 2123} 2124.post-media { 2125 margin: 12px 0; 2126 border-radius: 10px; 2127 overflow: hidden; 2128 max-width: 440px; 2129 display: flex; 2130 justify-content: center; 2131 background: #000; 2132} 2133.post-media img { 2134 max-width: 100%; 2135 max-height: 420px; 2136 width: auto; 2137 border-radius: 10px; 2138 cursor: pointer; 2139 object-fit: contain; 2140} 2141.post-media video { 2142 max-width: 100%; 2143 max-height: 420px; 2144 width: auto; 2145 border-radius: 10px; 2146 object-fit: contain; 2147} 2148@media (max-width: 600px) { 2149 .post-media { max-width: 100%; } 2150 .post-media img, 2151 .post-media video { max-height: 320px; } 2152} 2153.post-edit-btn { 2154 background: none; 2155 border: none; 2156 color: var(--plum-light); 2157 font-family: 'Marcellus', serif; 2158 font-size: 9px; 2159 letter-spacing: 1px; 2160 text-transform: uppercase; 2161 cursor: pointer; 2162 opacity: 0.7; 2163 transition: opacity 0.2s; 2164 padding: 2px 6px; 2165} 2166.post-edit-btn:hover { opacity: 1; color: var(--plum); } 2167.post-hide-btn { 2168 background: none; 2169 border: none; 2170 color: var(--plum-light); 2171 font-family: 'Marcellus', serif; 2172 font-size: 9px; 2173 letter-spacing: 1px; 2174 text-transform: uppercase; 2175 cursor: pointer; 2176 opacity: 0.7; 2177 transition: opacity 0.2s; 2178 padding: 2px 6px; 2179} 2180.post-hide-btn:hover { opacity: 1; color: var(--plum); } 2181 2182.edit-overlay { 2183 position: fixed; 2184 top: 0; left: 0; right: 0; bottom: 0; 2185 background: rgba(0,0,0,0.5); 2186 display: flex; 2187 align-items: center; 2188 justify-content: center; 2189 z-index: 1000; 2190} 2191 2192/* Toast notifications */ 2193.fr-toast-container { 2194 position: fixed; 2195 bottom: 24px; 2196 left: 50%; 2197 transform: translateX(-50%); 2198 z-index: 2000; 2199 display: flex; 2200 flex-direction: column; 2201 gap: 10px; 2202 pointer-events: none; 2203 width: calc(100% - 40px); 2204 max-width: 420px; 2205 align-items: center; 2206} 2207.fr-toast { 2208 background: var(--plum); 2209 color: var(--gold); 2210 padding: 14px 22px; 2211 border-radius: 10px; 2212 font-family: 'Lora', serif; 2213 font-size: 13px; 2214 line-height: 1.5; 2215 box-shadow: 0 6px 24px rgba(107, 26, 66, 0.35); 2216 border: 1px solid var(--gold-light); 2217 opacity: 0; 2218 transform: translateY(10px); 2219 transition: opacity 0.3s, transform 0.3s; 2220 pointer-events: auto; 2221 text-align: center; 2222 max-width: 100%; 2223} 2224.fr-toast.show { opacity: 1; transform: translateY(0); } 2225.fr-toast.error { background: var(--berry); } 2226.fr-toast.success { background: var(--plum); } 2227 2228/* Loading state for buttons */ 2229button.is-loading { 2230 opacity: 0.7; 2231 cursor: wait; 2232 position: relative; 2233} 2234button.is-loading::after {
2235 content: ''; 2236 display: inline-block; 2237 width: 8px; 2238 height: 8px; 2239 margin-left: 6px; 2240 border-radius: 50%; 2241 background: currentColor; 2242 animation: frPulse 0.9s ease-in-out infinite; 2243 vertical-align: middle; 2244} 2245@keyframes frPulse { 2246 0%, 100% { opacity: 0.3; transform: scale(0.8); } 2247 50% { opacity: 1; transform: scale(1.2); } 2248} 2249.edit-modal { 2250 background: var(--cream); 2251 border-radius: 14px; 2252 border: 2px solid var(--gold-pale); 2253 padding: 28px; 2254 width: 90%; 2255 max-width: 500px; 2256 box-shadow: 0 20px 60px rgba(0,0,0,0.2); 2257} 2258.edit-modal-title { 2259 font-family: 'Marcellus', serif; 2260 font-size: 9px; 2261 letter-spacing: 3px; 2262 text-transform: uppercase; 2263 color: var(--gold); 2264 margin-bottom: 14px; 2265} 2266.edit-modal textarea { 2267 width: 100%; 2268 min-height: 120px; 2269 padding: 12px; 2270 border: 1px solid rgba(197, 165, 90, 0.3); 2271 border-radius: 6px; 2272 background: white; 2273 font-family: 'Lora', serif; 2274 font-size: 13px; 2275 line-height: 1.75; 2276 color: var(--charcoal); 2277 outline: none; 2278 resize: vertical; 2279} 2280.edit-modal textarea:focus { border-color: var(--gold); } 2281.edit-modal-actions { 2282 display: flex; 2283 justify-content: flex-end; 2284 gap: 10px; 2285 margin-top: 14px; 2286} 2287.edit-modal-cancel { 2288 padding: 8px 18px; 2289 background: none; 2290 border: 1px solid rgba(197, 165, 90, 0.3); 2291 border-radius: 6px; 2292 font-family: 'Marcellus', serif; 2293 font-size: 9px; 2294 letter-spacing: 1.5px; 2295 text-transform: uppercase; 2296 color: var(--plum-light); 2297 cursor: pointer; 2298} 2299.edit-modal-save { 2300 padding: 8px 18px; 2301 background: var(--plum); 2302 border: none; 2303 border-radius: 6px; 2304 font-family: 'Marcellus', serif; 2305 font-size: 9px; 2306 letter-spacing: 1.5px; 2307 text-transform: uppercase; 2308 color: var(--gold); 2309 cursor: pointer; 2310} 2311.edit-modal-save:hover { background: var(--plum-deep); } 2312 2313.comment-media-btn { 2314 background: none; 2315 border: none; 2316 font-size: 16px; 2317 cursor: pointer; 2318 padding: 4px; 2319 opacity: 0.5; 2320 transition: opacity 0.2s; 2321} 2322.comment-media-btn:hover { opacity: 1; } 2323 2324/* Emoji Picker */ 2325.emoji-picker { 2326 position: absolute; 2327 bottom: 36px; 2328 left: 0; 2329 background: white; 2330 border: 1px solid rgba(197, 165, 90, 0.3); 2331 border-radius: 12px; 2332 box-shadow: 0 8px 30px rgba(0,0,0,0.12); 2333 padding: 12px; 2334 width: 280px; 2335 max-height: 260px; 2336 overflow-y: auto; 2337 z-index: 100; 2338} 2339.emoji-picker-grid { 2340 display: grid; 2341 grid-template-columns: repeat(8, 1fr); 2342 gap: 2px; 2343} 2344.emoji-btn { 2345 background: none; 2346 border: none; 2347 font-size: 20px; 2348 cursor: pointer; 2349 padding: 4px; 2350 border-radius: 6px; 2351 transition: background 0.15s; 2352} 2353.emoji-btn:hover { 2354 background: rgba(197, 165, 90, 0.15); 2355} 2356.emoji-category-label { 2357 font-family: 'Marcellus', serif; 2358 font-size: 8px; 2359 letter-spacing: 2px; 2360 text-transform: uppercase; 2361 color: var(--gold); 2362 margin: 8px 0 4px; 2363} 2364.emoji-category-label:first-child { margin-top: 0; } 2365 2366/* ============================================ 2367 FEED TOOLBAR (Filters + Search) 2368 ============================================ */ 2369.feed-toolbar { 2370 display: flex; 2371 align-items: center; 2372 justify-content: space-between; 2373 gap: 12px; 2374 margin-bottom: 20px; 2375 flex-wrap: wrap; 2376} 2377 2378.feed-filters { 2379 display: flex; 2380 gap: 4px; 2381} 2382 2383.feed-filter-btn { 2384 padding: 7px 16px; 2385 background: none; 2386 border: 1px solid var(--gold); 2387 border-radius: 20px; 2388 font-family: 'Marcellus', serif; 2389 font-size: 11px; 2390 letter-spacing: 1.2px; 2391 text-transform: uppercase; 2392 color: var(--plum-deep); 2393 font-weight: 600; 2394 cursor: pointer; 2395 transition: all 0.3s; 2396} 2397.feed-filter-btn:hover { 2398 border-color: var(--gold); 2399 color: var(--berry); 2400 background: rgba(197, 165, 90, 0.08); 2401} 2402.feed-filter-btn.active { 2403 background: var(--gold); 2404 border-color: var(--gold); 2405 color: white; 2406} 2407 2408.feed-search { 2409 position: relative; 2410 min-width: 200px; 2411} 2412.feed-search-input { 2413 width: 100%; 2414 padding: 7px 14px 7px 32px; 2415 border: 1px solid rgba(197, 165, 90, 0.25); 2416 border-radius: 20px; 2417 background: white; 2418 font-family: 'Lora', serif; 2419 font-size: 12px; 2420 color: var(--charcoal); 2421 outline: none; 2422 transition: border-color 0.3s; 2423} 2424.feed-search-input:focus { 2425 border-color: var(--gold); 2426} 2427.feed-search-input::placeholder { 2428 color: #C0B8A8; 2429 font-style: italic; 2430} 2431.feed-search-icon { 2432 position: absolute; 2433 left: 11px; 2434 top: 50%; 2435 transform: translateY(-50%); 2436 font-size: 13px; 2437 color: var(--gold-light); 2438 pointer-events: none; 2439} 2440.feed-search-clear { 2441 position: absolute; 2442 right: 8px; 2443 top: 50%; 2444 transform: translateY(-50%); 2445 background: none; 2446 border: none; 2447 font-size: 14px; 2448 color: var(--plum-light); 2449 cursor: pointer; 2450 display: none; 2451 padding: 2px 4px; 2452} 2453.feed-search-clear.show { display: block; } 2454 2455@media (max-width: 600px) { 2456 .feed-toolbar { flex-direction: column; align-items: stretch; } 2457 .feed-search { min-width: unset; } 2458} 2459 2460/* ============================================ 2461 REPLAYS / PLACEHOLDER VIEW 2462 ============================================ */ 2463.placeholder-view { 2464 text-align: center; 2465 padding: 80px 30px; 2466} 2467 2468.placeholder-icon { 2469 font-size: 48px; 2470 color: var(--gold-light); 2471 margin-bottom: 20px; 2472} 2473 2474.placeholder-title { 2475 font-family: 'Cormorant Garamond', serif; 2476 font-size: 26px; 2477 color: var(--plum-deep); 2478 font-weight: 400; 2479 margin-bottom: 10px; 2480} 2481 2482.placeholder-text { 2483 font-family: 'Cormorant Garamond', serif; 2484 font-style: italic; 2485 font-size: 16px; 2486 color: var(--plum); 2487 opacity: 0.6; 2488} 2489 2490/* ============================================ 2491 REPLAYS 2492 ============================================ */ 2493.replay-upload { 2494 background: white; 2495 border-radius: 14px; 2496 border: 1px solid rgba(197, 165, 90, 0.25); 2497 padding: 24px; 2498 margin-bottom: 24px; 2499} 2500.replay-upload-label { 2501 font-family: 'Marcellus', serif; 2502 font-size: 9px; 2503 letter-spacing: 3px; 2504 color: var(--gold); 2505 text-transform: uppercase; 2506 margin-bottom: 12px; 2507} 2508.replay-card { 2509 background: var(--cream-warm); 2510 border-radius: 12px; 2511 border: 1px solid rgba(197, 165, 90, 0.12); 2512 padding: 24px; 2513 margin-bottom: 18px; 2514} 2515.replay-card-title { 2516 font-family: 'Cormorant Garamond', serif; 2517 font-size: 22px; 2518 color: var(--plum-deep); 2519 font-weight: 400; 2520 margin-bottom: 6px; 2521} 2522.replay-card-desc { 2523 font-family: 'Lora', serif; 2524 font-size: 13px; 2525 line-height: 1.6; 2526 color: var(--charcoal); 2527 opacity: 0.8; 2528 margin-bottom: 14px; 2529} 2530.replay-card-date { 2531 font-family: 'Marcellus', serif; 2532 font-size: 9px; 2533 letter-spacing: 2px; 2534 color: var(--berry); 2535 text-transform: uppercase; 2536 margin-bottom: 12px; 2537} 2538.replay-card video { 2539 max-width: 440px; 2540 width: 100%; 2541 max-height: 420px; 2542 border-radius: 10px; 2543 margin-top: 10px; 2544 display: block; 2545 background: #000; 2546 object-fit: contain; 2547} 2548@media (max-width: 600px) { 2549 .replay-card video { 2550 max-width: 100%; 2551 max-height: 320px; 2552 } 2553} 2554.replay-card-actions { 2555 display: flex; 2556 gap: 8px; 2557 margin-top: 12px; 2558 justify-content: flex-end; 2559} 2560.replay-delete-btn { 2561 padding: 4px 12px; 2562 border-radius: 14px; 2563 font-family: 'Marcellus', serif; 2564 font-size: 8px; 2565 letter-spacing: 1.5px; 2566 text-transform: uppercase; 2567 cursor: pointer; 2568 background: none; 2569 border: 1px solid rgba(148, 14, 75, 0.3); 2570 color: var(--berry); 2571 transition: all 0.3s; 2572} 2573.replay-delete-btn:hover { 2574 background: var(--berry); 2575 color: white; 2576} 2577 2578/* ============================================ 2579 TOOLS & GUIDES 2580 ============================================ */ 2581.tools-grid { 2582 display: flex; 2583 flex-direction: column; 2584 gap: 16px; 2585} 2586.tool-card { 2587 background: white; 2588 border-radius: 14px; 2589 border: 1px solid rgba(197, 165, 90, 0.2); 2590 overflow: hidden; 2591 cursor: pointer; 2592 transition: border-color 0.3s, box-shadow 0.3s; 2593} 2594.tool-card:hover { 2595 border-color: var(--gold-light); 2596 box-shadow: 0 2px 16px rgba(197, 165, 90, 0.1); 2597} 2598.tool-card-header { 2599 display: flex; 2600 align-items: center; 2601 justify-content: space-between; 2602 padding: 20px 24px; 2603} 2604.tool-card-header-left { 2605 display: flex; 2606 align-items: center; 2607 gap: 14px; 2608} 2609.tool-card-icon { 2610 font-size: 24px; 2611 color: var(--gold); 2612} 2613.tool-card-title { 2614 font-family: 'Cormorant Garamond', serif; 2615 font-size: 20px; 2616 color: var(--plum-deep); 2617 font-weight: 400; 2618} 2619.tool-card-subtitle { 2620 font-family: 'Lora', serif; 2621 font-size: 11px; 2622 color: var(--charcoal); 2623 opacity: 0.5; 2624 margin-top: 2px; 2625} 2626.tool-card-arrow { 2627 font-size: 16px; 2628 color: var(--gold-light); 2629 transition: transform 0.3s; 2630} 2631.tool-card.open .tool-card-arrow { 2632 transform: rotate(90deg); 2633} 2634.tool-card-content { 2635 display: none; 2636 padding: 0 24px 24px; 2637 border-top: 1px solid rgba(197, 165, 90, 0.1); 2638} 2639.tool-card.open .tool-card-content { 2640 display: block; 2641} 2642.guide-section { 2643 margin-top: 18px; 2644} 2645.guide-section-title { 2646 font-family: 'Marcellus', serif; 2647 font-size: 10px; 2648 letter-spacing: 2px; 2649 text-transform: uppercase; 2650 color: var(--berry); 2651 margin-bottom: 10px; 2652} 2653.guide-step { 2654 display: flex; 2655 gap: 12px; 2656 margin-bottom: 14px; 2657} 2658.guide-step-num { 2659 flex-shrink: 0; 2660 width: 26px;
2661 height: 26px; 2662 border-radius: 50%; 2663 background: var(--gold); 2664 color: white; 2665 font-family: 'Marcellus', serif; 2666 font-size: 12px; 2667 display: flex; 2668 align-items: center; 2669 justify-content: center; 2670 margin-top: 2px; 2671} 2672.guide-step-text { 2673 font-family: 'Lora', serif; 2674 font-size: 13px; 2675 line-height: 1.65; 2676 color: var(--charcoal); 2677} 2678.guide-step-text strong { 2679 color: var(--plum-deep); 2680} 2681.guide-note { 2682 margin-top: 14px; 2683 padding: 14px 16px; 2684 background: var(--cream-warm); 2685 border-radius: 8px; 2686 border-left: 3px solid var(--gold); 2687 font-family: 'Lora', serif; 2688 font-size: 12px; 2689 line-height: 1.6; 2690 color: var(--charcoal); 2691 opacity: 0.85; 2692} 2693.guide-note strong { 2694 color: var(--plum-deep); 2695} 2696.guide-divider { 2697 height: 1px; 2698 background: rgba(197, 165, 90, 0.15); 2699 margin: 20px 0; 2700} 2701 2702/* ============================================ 2703 LOADING 2704 ============================================ */ 2705.loading { 2706 display: flex; 2707 align-items: center; 2708 justify-content: center; 2709 padding: 60px 20px; 2710 gap: 8px; 2711} 2712 2713.loading-dot { 2714 width: 8px; 2715 height: 8px; 2716 background: var(--gold-light); 2717 border-radius: 50%; 2718 animation: loadingPulse 1.2s infinite; 2719} 2720 2721.loading-dot:nth-child(2) { animation-delay: 0.2s; } 2722.loading-dot:nth-child(3) { animation-delay: 0.4s; } 2723 2724@keyframes loadingPulse { 2725 0%, 100% { opacity: 0.3; transform: scale(0.8); } 2726 50% { opacity: 1; transform: scale(1); } 2727} 2728 2729/* ============================================ 2730 RESPONSIVE 2731 ============================================ */ 2732@media (max-width: 768px) { 2733 .sidebar { 2734 transform: translateX(-100%); 2735 } 2736 2737 .sidebar.open { 2738 transform: translateX(0); 2739 } 2740 2741 .hamburger { 2742 display: flex; 2743 } 2744 2745 .sidebar-overlay.show { 2746 display: block; 2747 } 2748 2749 .content-area { 2750 margin-left: 0; 2751 } 2752 2753 .content-header { 2754 padding-left: 70px; 2755 } 2756 2757 .password-card, 2758 .auth-card { padding: 40px 30px 36px; } 2759 2760 .content-header-title { font-size: 24px; } 2761 2762 /* More legible sidebar nav on mobile */ 2763 .sidebar-link { 2764 font-size: 11px; 2765 letter-spacing: 1.5px; 2766 padding: 14px 22px; 2767 } 2768 2769 /* Prevent iOS Safari from auto-zooming when focusing form fields. 2770 iOS only zooms when the input font-size is < 16px. */ 2771 input[type="text"], 2772 input[type="email"], 2773 input[type="password"], 2774 input[type="search"], 2775 input[type="number"], 2776 input[type="tel"], 2777 input[type="url"], 2778 textarea, 2779 select { 2780 font-size: 16px !important; 2781 } 2782} 2783 2784@media (max-width: 480px) { 2785 .post-card { padding: 24px 20px 18px; } 2786 .composer { padding: 20px; } 2787 .post-meta { flex-direction: column; gap: 10px; align-items: flex-start; } 2788} 2789 2790/* Print Journal Button */ 2791.journal-print-btn { 2792 display: inline-flex; 2793 align-items: center; 2794 gap: 6px; 2795 padding: 8px 20px; 2796 background: none; 2797 border: 1.5px solid var(--gold); 2798 border-radius: 8px; 2799 color: var(--gold); 2800 font-family: 'Marcellus', serif; 2801 font-size: 9px; 2802 letter-spacing: 2px; 2803 text-transform: uppercase; 2804 cursor: pointer; 2805 transition: all 0.3s; 2806 margin-bottom: 20px; 2807} 2808.journal-print-btn:hover { 2809 background: var(--gold); 2810 color: white; 2811} 2812.journal-print-btn .print-icon { 2813 font-size: 14px; 2814} 2815 2816/* ============================================ 2817 PRINT STYLES â BRANDED JOURNAL 2818 ============================================ */ 2819@media print { 2820 /* Hide everything except journal content */ 2821 body * { visibility: hidden; } 2822 #journal-print-area, #journal-print-area * { visibility: visible; } 2823 #journal-print-area { 2824 position: absolute; 2825 top: 0; 2826 left: 0; 2827 width: 100%; 2828 padding: 0; 2829 margin: 0; 2830 } 2831 2832 .journal-print-header { 2833 text-align: center; 2834 padding: 40px 0 30px; 2835 border-bottom: 2px solid #C5A55A; 2836 margin-bottom: 30px; 2837 } 2838 .journal-print-logo { 2839 width: 70px; 2840 height: auto; 2841 margin-bottom: 12px; 2842 } 2843 .journal-print-brand { 2844 font-family: 'Marcellus', serif; 2845 font-size: 9px; 2846 letter-spacing: 4px; 2847 text-transform: uppercase; 2848 color: #940E4B; 2849 margin-bottom: 4px; 2850 } 2851 .journal-print-title { 2852 font-family: 'Cormorant Garamond', serif; 2853 font-size: 28px; 2854 font-weight: 400; 2855 color: #3A3A3A; 2856 margin: 4px 0 0; 2857 } 2858 .journal-print-name { 2859 font-family: 'Cormorant Garamond', serif; 2860 font-size: 16px; 2861 font-style: italic; 2862 color: #C5A55A; 2863 margin-top: 8px; 2864 } 2865 .journal-print-entry { 2866 padding: 22px 0; 2867 border-bottom: 1px solid rgba(197, 165, 90, 0.3); 2868 page-break-inside: avoid; 2869 } 2870 .journal-print-entry:last-child { 2871 border-bottom: none; 2872 } 2873 .journal-print-date { 2874 font-family: 'Marcellus', serif; 2875 font-size: 8px; 2876 letter-spacing: 2.5px; 2877 text-transform: uppercase; 2878 color: #C5A55A; 2879 margin-bottom: 8px; 2880 } 2881 .journal-print-body { 2882 font-family: 'Lora', serif; 2883 font-size: 12px; 2884 line-height: 1.85; 2885 color: #3A3A3A; 2886 white-space: pre-wrap; 2887 } 2888 .journal-print-footer { 2889 text-align: center; 2890 margin-top: 40px; 2891 padding-top: 20px; 2892 border-top: 2px solid #C5A55A; 2893 } 2894 .journal-print-footer-text { 2895 font-family: 'Cormorant Garamond', serif; 2896 font-size: 11px; 2897 font-style: italic; 2898 color: #C5A55A; 2899 letter-spacing: 1px; 2900 } 2901 .journal-print-diamond { 2902 color: #C5A55A; 2903 font-size: 14px; 2904 margin-bottom: 8px; 2905 } 2906} 2907 2908/* ============================================ 2909 HOME / WELCOME VIEW 2910 ============================================ */ 2911.home-view { 2912 display: flex; 2913 flex-direction: column; 2914 align-items: center; 2915 justify-content: center; 2916 min-height: calc(100vh - 120px); 2917 padding: 40px 30px; 2918 text-align: center; 2919} 2920.home-logo { 2921 width: 110px; 2922 height: 110px; 2923 margin-bottom: 28px; 2924 opacity: 0.9; 2925 animation: homeLogoFade 1.5s ease-in-out; 2926} 2927 2928/* ============================================ 2929 ONBOARDING POPUP â first-login nudge 2930 ============================================ */ 2931.onboarding-overlay { 2932 position: fixed; 2933 inset: 0; 2934 z-index: 300; 2935 pointer-events: none; 2936 opacity: 0; 2937 animation: onboardFadeIn 1.5s ease 6s forwards; 2938} 2939 2940@keyframes onboardFadeIn { 2941 to { opacity: 1; } 2942} 2943 2944.onboarding-popup { 2945 position: fixed; 2946 top: 50%;
2947 left: calc(var(--sidebar-width) + 60px); 2948 transform: translateY(-50%); 2949 background: white; 2950 border: 1px solid rgba(197, 165, 90, 0.3); 2951 border-radius: 16px; 2952 padding: 32px 36px; 2953 box-shadow: 0 8px 40px rgba(139, 34, 82, 0.12); 2954 z-index: 301; 2955 max-width: 340px; 2956 text-align: center; 2957 pointer-events: auto; 2958} 2959 2960.onboarding-popup-title { 2961 font-family: 'Cormorant Garamond', serif; 2962 font-size: 22px; 2963 color: var(--berry); 2964 font-weight: 600; 2965 margin-bottom: 10px; 2966 line-height: 1.3; 2967} 2968 2969.onboarding-popup-text { 2970 font-family: 'Lora', serif; 2971 font-size: 13px; 2972 line-height: 1.7; 2973 color: var(--charcoal); 2974 opacity: 0.85; 2975} 2976 2977/* Animated arrow pointing left toward the sidebar */ 2978.onboarding-arrow { 2979 position: fixed; 2980 top: 50%; 2981 left: calc(var(--sidebar-width) + 24px); 2982 transform: translateY(-50%); 2983 z-index: 302; 2984 pointer-events: none; 2985 animation: arrowBounce 1.2s ease-in-out infinite; 2986} 2987 2988.onboarding-arrow svg { 2989 width: 28px; 2990 height: 28px; 2991 fill: none; 2992 stroke: var(--gold); 2993 stroke-width: 2.5; 2994 stroke-linecap: round; 2995 stroke-linejoin: round; 2996 filter: drop-shadow(0 0 6px rgba(197, 165, 90, 0.5)); 2997} 2998 2999@keyframes arrowBounce { 3000 0%, 100% { transform: translateY(-50%) translateX(0); } 3001 50% { transform: translateY(-50%) translateX(-12px); } 3002} 3003 3004/* Fade out */ 3005.onboarding-overlay.fade-out { 3006 transition: opacity 1.2s ease; 3007 opacity: 0 !important; 3008} 3009 3010@media (max-width: 768px) { 3011 .onboarding-popup { 3012 left: 50%; 3013 transform: translate(-50%, -50%); 3014 top: 40%; 3015 max-width: 280px; 3016 padding: 24px 28px; 3017 } 3018 .onboarding-arrow { 3019 display: none; 3020 } 3021} 3022 3023/* Gold sparkles that pour outward when entering the hub 3024 Built as inline SVGs for soft glow + twinkle */ 3025.welcome-star { 3026 position: fixed; 3027 pointer-events: none; 3028 will-change: transform, opacity; 3029 z-index: 50; /* in front of tiles by default */ 3030 user-select: none; 3031 display: flex; 3032 align-items: center; 3033 justify-content: center; 3034} 3035/* "Behind" sparkles drift below the tiles for parallax depth */ 3036.welcome-star.behind { 3037 z-index: 1; 3038 filter: blur(0.6px); /* slightly soft, like distant points of light */ 3039} 3040.welcome-star.behind svg { 3041 filter: drop-shadow(0 0 6px rgba(197, 165, 90, 0.7)) 3042 drop-shadow(0 0 14px rgba(212, 186, 122, 0.45)); 3043 opacity: 0.85; 3044} 3045.welcome-star svg { 3046 width: 100%; 3047 height: 100%; 3048 filter: drop-shadow(0 0 4px rgba(197, 165, 90, 0.8)) 3049 drop-shadow(0 0 10px rgba(212, 186, 122, 0.5)); 3050 animation: twinkle 1.8s ease-in-out infinite; 3051} 3052@keyframes twinkle { 3053 0%, 100% { opacity: 1; transform: scale(1); } 3054 50% { opacity: 0.55; transform: scale(0.78); } 3055} 3056.home-greeting { 3057 font-family: 'Marcellus', serif; 3058 font-size: 12px; 3059 letter-spacing: 5px; 3060 text-transform: uppercase; 3061 color: var(--gold); 3062 margin-bottom: 10px; 3063 animation: homeTextFade 1.8s ease-in-out; 3064} 3065.home-name { 3066 font-family: 'Great Vibes', cursive; 3067 font-size: 52px; 3068 color: #A83060; 3069 margin-bottom: 28px; 3070 animation: homeTextFade 2.2s ease-in-out; 3071} 3072.home-divider { 3073 display: flex; 3074 align-items: center; 3075 gap: 16px; 3076 margin-bottom: 28px; 3077 animation: homeTextFade 2.5s ease-in-out; 3078} 3079.home-divider-line { 3080 width: 60px; 3081 height: 1px; 3082 background: var(--gold-light); 3083} 3084.home-divider-diamond { 3085 color: var(--gold); 3086 font-size: 10px; 3087} 3088.home-message { 3089 font-family: 'Cormorant Garamond', serif; 3090 font-style: italic; 3091 font-size: 20px; 3092 line-height: 1.7; 3093 color: var(--plum); 3094 max-width: 520px; 3095 margin-bottom: 40px; 3096 animation: homeTextFade 2.8s ease-in-out; 3097} 3098.home-rooms { 3099 display: grid; 3100 grid-template-columns: repeat(3, 1fr); 3101 gap: 14px; 3102 max-width: 540px; 3103 width: 100%; 3104 animation: homeTextFade 3.2s ease-in-out; 3105} 3106@media (max-width: 500px) { 3107 .home-rooms { 3108 grid-template-columns: repeat(2, 1fr); 3109 } 3110} 3111.home-room-card { 3112 background: white; 3113 border: 1px solid rgba(197, 165, 90, 0.18); 3114 border-radius: 12px; 3115 padding: 22px 16px; 3116 cursor: pointer; 3117 transition: all 0.3s; 3118 text-align: center; 3119 /* Sit above the "behind" sparkles but below the "front" ones for parallax depth */ 3120 position: relative; 3121 z-index: 10; 3122} 3123.home-room-card:hover { 3124 border-color: var(--gold); 3125 box-shadow: 0 4px 20px rgba(197, 165, 90, 0.12); 3126 transform: translateY(-2px); 3127} 3128.home-room-icon { 3129 font-size: 22px; 3130 color: var(--gold); 3131 margin-bottom: 8px; 3132} 3133.home-room-title { 3134 font-family: 'Marcellus', serif; 3135 font-size: 11.5px; 3136 letter-spacing: 1.5px; 3137 text-transform: uppercase; 3138 color: var(--berry); 3139 font-weight: 600; 3140 margin-bottom: 6px; 3141} 3142.home-room-desc { 3143 font-family: 'Lora', serif; 3144 font-size: 12px; 3145 color: var(--plum); 3146 opacity: 0.65; 3147 line-height: 1.45; 3148 font-style: italic; 3149} 3150@keyframes homeLogoFade { 3151 0% { opacity: 0; transform: scale(0.9); } 3152 100% { opacity: 0.9; transform: scale(1); } 3153} 3154@keyframes homeTextFade { 3155 0%, 30% { opacity: 0; transform: translateY(8px); } 3156 100% { opacity: 1; transform: translateY(0); } 3157} 3158@media (max-width: 600px) { 3159 #members-stats { grid-template-columns: 1fr !important; } 3160 #library-analytics-stats { grid-template-columns: 1fr !important; } 3161} 3162</style> 3163</head> 3164<body> 3165 3166<!-- ============================================ 3167 PASSWORD SCREEN 3168 ============================================ --> 3169<div id="password-screen"> 3170 <a class='back-to-portal' href='/'>← Back to Main Portal</a> 3171 <div class="password-card"> 3172 <img src="feminine-rising-logo.png" alt="Feminine Rising" class="password-logo"> 3173 <div class="password-title">
3173Feminine Rising</div> 3174 <h1 class="password-heading">Private Mentorship</h1> 3175 <div class="password-subtitle">Exclusive Client Resources</div> 3176 3177 <div class="password-divider"><span>♦︎</span></div> 3178 3179 <p class="password-prompt">Please enter your mentorship access code</p> 3180 <input type="text" id="password-input" class="password-input" placeholder="Access Code" autofocus> 3181 <button onclick="checkPassword()" class="password-btn">Enter</button> 3182 <p id="password-error" class="password-error">The access code you entered is not recognized. Please try again.</p> 3183 </div> 3184 <a class='back-link' href='/'>← Back to Main Portal</a> 3185</div> 3186 3187<!-- ============================================ 3188 AUTH SCREEN (Email + Password) 3189 ============================================ --> 3190<div id="auth-screen"> 3191 <a class='back-to-portal' href='/'>← Back to Main Portal</a> 3192 <div class="auth-card"> 3193 <div class="auth-icon">✦</div> 3194 <h2 class="auth-title">Welcome, Sister</h2> 3195 <p class="auth-text" id="auth-subtitle"> 3196 Create your account to enter the mentorship hub. 3197 </p> 3198 <input type="email" id="auth-email" class="auth-input" placeholder="[email protected]" autocomplete="email" autocapitalize="none" autocorrect="off" spellcheck="false"> 3199 <div style="position:relative;margin-top:10px;"> 3200 <input type="password" id="auth-password" class="auth-input" placeholder="Create a password" autocomplete="current-password" autocapitalize="none" autocorrect="off" spellcheck="false" style="padding-right:46px;"> 3201 <button type="button" onclick="toggleAuthPasswordVisibility()" aria-label="Show or hide password" style="position:absolute;right:12px;top:50%;transform:translateY(-50%);background:none;border:none;cursor:pointer;font-size:18px;color:var(--gold);padding:4px 8px;line-height:1;"> 3202 <span id="auth-eye-icon">👁</span> 3203 </button> 3204 </div> 3205 <button id="auth-send-btn" onclick="authSubmit()" class="auth-btn">Create My Account</button> 3206 <p id="auth-message" class="auth-message"></p> 3207 <div style="margin-top:18px;"> 3208 <button id="auth-toggle" onclick="toggleAuthMode()" style="background:none;border:none;color:var(--plum-light);font-family:'Cormorant Garamond',serif;font-size:14px;font-style:italic;cursor:pointer;letter-spacing:0.3px;opacity:0.85;transition:opacity 0.2s;" onmouseover="this.style.opacity='1'" onmouseout="this.style.opacity='0.85'">Already have an account? Sign in</button> 3209 </div> 3210 <div id="forgot-row" style="margin-top:10px;display:none;"> 3211 <button onclick="sendPasswordReset()" style="background:none;border:none;color:var(--gold);font-family:'Marcellus',serif;font-size:10px;letter-spacing:2px;cursor:pointer;text-transform:uppercase;opacity:0.8;transition:opacity 0.2s;" onmouseover="this.style.opacity='1'" onmouseout="this.style.opacity='0.8'">Forgot password?</button> 3212 </div> 3213 </div> 3214</div> 3215 3216<!-- ============================================ 3217 RESET PASSWORD SCREEN (after clicking email link) 3218 ============================================ --> 3219<div id="reset-screen" style="display:none;position:fixed;top:0;left:0;right:0;bottom:0;align-items:center;justify-content:center;background:var(--cream);z-index:50;"> 3220 <div class="auth-card"> 3221 <div class="auth-icon">✦</div> 3222 <h2 class="auth-title">Set a New Password</h2> 3223 <p class="auth-text">Choose a new password for your account.</p> 3224 <div style="position:relative;"> 3225 <input type="password" id="reset-password" class="auth-input" placeholder="New password (8+ characters)" autocomplete="new-password" autocapitalize="none" autocorrect="off" spellcheck="false" style="padding-right:46px;"> 3226 <button type="button" onclick="toggleResetPasswordVisibility()" aria-label="Show or hide password" style="position:absolute;right:12px;top:50%;transform:translateY(-50%);background:none;border:none;cursor:pointer;font-size:18px;color:var(--gold);padding:4px 8px;line-height:1;"> 3227 <span id="reset-eye-icon">👁</span> 3228 </button> 3229 </div> 3230 <div style="position:relative;margin-top:10px;"> 3231 <input type="password" id="reset-password-confirm" class="auth-input" placeholder="Confirm new password" autocomplete="new-password" autocapitalize="none" autocorrect="off" spellcheck="false" style="padding-right:46px;"> 3232 <button type="button" onclick="toggleResetPasswordVisibility()" aria-label="Show or hide password" style="position:absolute;right:12px;top:50%;transform:translateY(-50%);background:none;border:none;cursor:pointer;font-size:18px;color:var(--gold);padding:4px 8px;line-height:1;">
3233 <span id="reset-eye-icon-2">👁</span> 3234 </button> 3235 </div> 3236 <button id="reset-submit-btn" onclick="updatePasswordSubmit()" class="auth-btn" style="margin-top:14px;">Update Password</button> 3237 <p id="reset-message" class="auth-message"></p> 3238 </div> 3239</div> 3240 3241<!-- ============================================ 3242 NAME ONBOARDING 3243 ============================================ --> 3244<div id="name-screen"> 3245 <div class="auth-card"> 3246 <div class="auth-icon">✦</div> 3247 <h2 class="auth-title">One Last Thing</h2> 3248 <p class="auth-text"> 3249 What name shall we call you in circle? 3250 </p> 3251 <input type="text" id="name-input" class="auth-input" placeholder="Your name" style="text-align:center;"> 3252 <button onclick="saveName()" class="auth-btn">Enter the Circle</button> 3253 </div> 3254</div> 3255 3256<!-- ============================================ 3257 THE HUB 3258 ============================================ --> 3259<div id="hub"> 3260 <div class="hub-layout"> 3261 3262 <!-- Mobile hamburger --> 3263 <button class="hamburger" onclick="toggleSidebar()"> 3264 <span></span><span></span><span></span> 3265 </button> 3266 <div class="sidebar-overlay" onclick="toggleSidebar()"></div> 3267 3268 <!-- Sidebar --> 3269 <nav class="sidebar"> 3270 <div class="sidebar-header"> 3271 <img src="feminine-rising-logo.png" alt="Feminine Rising" class="sidebar-logo"> 3272 <div class="sidebar-brand">
3272Feminine Rising</div> 3273 <div class="sidebar-title">Private Mentorship</div> 3274 </div> 3275 3276 <div class="sidebar-nav"> 3277 <a class="sidebar-link active" data-view="home" onclick="switchView('home')"> 3278 <span class="sidebar-link-icon">✿</span> Home 3279 </a> 3280 <a class="sidebar-link" data-view="about" onclick="switchView('about')"> 3281 <span class="sidebar-link-icon" style="line-height:1;display:inline-flex;align-items:center;"><svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M12 5c-1.5-1.5-4-2.5-7-2.5v15c3 0 5.5 1 7 2.5"/><path d="M12 5c1.5-1.5 4-2.5 7-2.5v15c-3 0-5.5 1-7 2.5"/><path d="M12 5v15"/></svg></span> About 3282 </a> 3283 <a class="sidebar-link" data-view="feed" onclick="switchView('feed')"> 3284 <span class="sidebar-link-icon" style="line-height:1;display:inline-flex;align-items:center;"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M12 12c-2-2.5-4-4-6-4c-2.2 0-4 1.8-4 4s1.8 4 4 4c2 0 4-1.5 6-4z"/><path d="M12 12c2 2.5 4 4 6 4c2.2 0 4-1.8 4-4s-1.8-4-4-4c-2 0-4 1.5-6 4z"/></svg></span> Community 3285 </a> 3286 <a class="sidebar-link" data-view="teachings" onclick="switchView('teachings')"> 3287 <span class="sidebar-link-icon">✦</span> Teachings 3288 </a> 3289 <a class="sidebar-link" data-view="replays" onclick="switchView('replays')"> 3290 <span class="sidebar-link-icon">▶</span> Replays 3291 </a> 3292 <a class="sidebar-link" data-view="journal" onclick="switchView('journal')"> 3293 <span class="sidebar-link-icon">✎</span> Journal 3294 </a> 3295 <a class="sidebar-link" data-view="harvest" onclick="switchView('harvest')"> 3296 <span class="sidebar-link-icon"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M12 2c-3 4-6 7-6 12a6 6 0 0 0 12 0c0-5-3-8-6-12z"/><path d="M12 14v6"/><path d="M9 18c1.5-1 4.5-1 6 0"/></svg></span> Monthly Harvest 3297 </a> 3298 <a class="sidebar-link" data-view="schedule" onclick="switchView('schedule')"> 3299 <span class="sidebar-link-icon">☷</span> Schedule 3300 </a> 3301 <a class="sidebar-link" data-view="tools" onclick="switchView('tools')"> 3302 <span class="sidebar-link-icon">⚙</span> Tools 3303 </a> 3304 <a class="sidebar-link" data-view="members" onclick="switchView('members')" id="sidebar-members-link" style="display:none;"> 3305 <span class="sidebar-link-icon">★</span> Members 3306 </a> 3307 <a class='sidebar-link' href='/crm' id='sidebar-crm-link' style='display:none;'> 3308 <span class="sidebar-link-icon">◉</span> CRM 3309 </a> 3310 </div> 3311 3312 <div class="sidebar-footer"> 3313 <div class="sidebar-user" id="sidebar-user-name"> 3314 <span id="sidebar-user-name-text">Sister</span> 3315 <span id="sidebar-presence-dot" class="sidebar-presence-dot" style="display:none;" title="Sumir is here"></span> 3316 </div> 3317 <!-- Teacher-only presence toggle --> 3318 <button id="sidebar-presence-toggle" onclick="togglePresence()" class="sidebar-presence-toggle" style="display:none;"> 3319 <span id="sidebar-presence-label">♡ Mark as Here</span> 3320 </button> 3321 <button class="sidebar-signout" onclick="signOut()">Sign Out</button> 3322 <a class='sidebar-back' href='/'>← Back to Main Portal</a> 3323 </div> 3324 </nav> 3325 3326 <!-- Content area --> 3327 <main class="content-area"> 3328 3329 <!-- Dynamic header --> 3330 <header class="content-header"> 3331 <div class="content-header-eyebrow" id="header-eyebrow">Private Mentorship</div> 3332 <h1 class="content-header-title" id="header-title">Community</h1> 3333 </header> 3334 3335 <!-- ===== HOME VIEW ===== --> 3336 <div class="view" id="view-home"> 3337 <div class="home-view"> 3338 <!-- "Sumir is in the room" banner --> 3339 <div id="presence-banner" class="presence-banner" style="display:none;">
3340 <span class="presence-banner-dot"></span> 3341 <span class="presence-banner-text">Sumir is in the room</span> 3342 </div> 3343 <img src="feminine-rising-logo.png" alt="Feminine Rising" class="home-logo" id="home-logo"> 3344 <div class="home-greeting" id="home-greeting">Welcome</div> 3345 <div class="home-name" id="home-user-name">Sister</div> 3346 <div class="home-divider"> 3347 <div class="home-divider-line"></div> 3348 <div class="home-divider-diamond">✦</div> 3349 <div class="home-divider-line"></div> 3350 </div> 3351 <div class="home-rooms"> 3352 <div class="home-room-card" onclick="switchView('feed')"> 3353 <div class="home-room-icon"><svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 12c-2-2.5-4-4-6-4c-2.2 0-4 1.8-4 4s1.8 4 4 4c2 0 4-1.5 6-4z"/><path d="M12 12c2 2.5 4 4 6 4c2.2 0 4-1.8 4-4s-1.8-4-4-4c-2 0-4 1.5-6 4z"/></svg></div> 3354 <div class="home-room-title">Community</div> 3355 <div class="home-room-desc">Our shared space</div> 3356 </div> 3357 <div class="home-room-card" onclick="switchView('teachings')"> 3358 <div class="home-room-icon">✦</div> 3359 <div class="home-room-title">Teachings</div> 3360 <div class="home-room-desc">Fertile Ground</div> 3361 </div> 3362 <div class="home-room-card" onclick="switchView('replays')"> 3363 <div class="home-room-icon">▶</div> 3364 <div class="home-room-title">Replays</div> 3365 <div class="home-room-desc">Session recordings</div> 3366 </div> 3367 <div class="home-room-card" onclick="switchView('harvest')"> 3368 <div class="home-room-icon"><svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 2c-3 4-6 7-6 12a6 6 0 0 0 12 0c0-5-3-8-6-12z"/><path d="M12 14v6"/><path d="M9 18c1.5-1 4.5-1 6 0"/></svg></div> 3369 <div class="home-room-title">Monthly Harvest</div> 3370 <div class="home-room-desc">Reflect & gather</div> 3371 </div> 3372 <div class="home-room-card" onclick="switchView('journal')"> 3373 <div class="home-room-icon">✎</div> 3374 <div class="home-room-title">Journal</div> 3375 <div class="home-room-desc">Your sacred pages</div> 3376 </div> 3377 <div class="home-room-card" onclick="switchView('schedule')"> 3378 <div class="home-room-icon">☷</div> 3379 <div class="home-room-title">Schedule</div> 3380 <div class="home-room-desc">Upcoming sessions</div> 3381 </div> 3382 </div> 3383 </div> 3384 </div> 3385 3386 <!-- ===== COMMUNITY FEED VIEW ===== --> 3387 <div class="view active" id="view-feed"> 3388 <div class="content-body"> 3389 3390 <!-- Composer (teacher only) --> 3391 <div class="composer" id="post-composer" style="display:none;"> 3392 <div class="composer-trigger" onclick="toggleComposer()"> 3393 <div class="composer-trigger-avatar" id="composer-avatar">S</div> 3394 <span class="composer-trigger-text">Share something with the community...</span> 3395 </div> 3396 <div class="composer-expanded" id="composer-expanded"> 3397 <div style="display:flex;justify-content:space-between;align-items:center;"> 3398 <div class="composer-label" style="padding-top:18px;">Share With Us</div> 3399 <button onclick="closeComposer()" style="background:none;border:none;font-size:20px;color:var(--plum-light);cursor:pointer;padding:12px 4px 0;opacity:0.5;transition:opacity 0.2s;" onmouseover="this.style.opacity='1'" onmouseout="this.style.opacity='0.5'">×</button> 3400 </div> 3401 <input type="text" class="composer-title-input" id="compose-title" placeholder="Title (optional)"> 3402 <textarea class="composer-textarea" id="compose-body" placeholder="What would you like to share..."></textarea> 3403 <div id="compose-media-preview" class="media-preview" style="display:none;"></div> 3404 <input type="file" id="compose-media-input" accept="image/*,video/*" style="display:none;" onchange="handleMediaSelect(this)"> 3405 <div class="composer-footer"> 3406 <div style="display:flex;align-items:center;gap:8px;"> 3407 <select class="composer-type" id="compose-type"> 3408 <option value="teaching" class="teacher-only-option">Teaching</option> 3409 <option value="announcement" class="teacher-only-option">Announcement</option> 3410 <option value="reflection">Reflection</option> 3411 <option value="prompt">Prompt</option> 3412 </select> 3413 <button class="media-upload-btn" onclick="document.getElementById('compose-media-input').click()">
3414 <span class="upload-icon">📷</span> Photo / Video 3415 </button> 3416 <button class="media-upload-btn" onclick="openVoiceRecorder()" title="Record a voice note (5 min max)"> 3417 <span class="upload-icon">🎤</span> Voice Note 3418 </button> 3419 <div style="position:relative;display:inline-block;"> 3420 <button class="media-upload-btn" onclick="toggleEmojiPicker('compose-body')"> 3421 <span class="upload-icon">😊</span> Emoji 3422 </button> 3423 <div class="emoji-picker" id="emoji-picker-compose-body" style="display:none;"></div> 3424 </div> 3425 </div> 3426 <button class="composer-submit" id="compose-submit" onclick="createPost()">Share</button> 3427 </div> 3428 <!-- Voice note preview (shown after recording) --> 3429 <div id="voice-note-preview" style="display:none;margin-top:10px;padding:14px 16px;background:var(--cream-warm);border:1px solid var(--gold);border-radius:10px;"> 3430 <div style="display:flex;align-items:center;gap:10px;"> 3431 <span style="font-size:20px;">🎤</span> 3432 <audio id="voice-note-audio" controls style="flex:1;height:36px;"></audio> 3433 <button onclick="clearVoiceNote()" title="Remove" style="background:none;border:none;color:var(--berry);font-size:18px;cursor:pointer;padding:4px 8px;">×</button> 3434 </div> 3435 <div id="voice-note-duration" style="font-family:'Marcellus',serif;font-size:10px;letter-spacing:1.5px;color:var(--gold);margin-top:6px;"></div> 3436 </div> 3437 </div> 3438 </div> 3439 3440 <!-- Voice Recorder Modal --> 3441 <div id="voice-recorder-modal" style="display:none;position:fixed;inset:0;background:rgba(0,0,0,0.55);z-index:2000;align-items:center;justify-content:center;padding:20px;"> 3442 <div style="background:white;border-radius:18px;padding:40px 36px;max-width:420px;width:100%;text-align:center;box-shadow:0 12px 40px rgba(0,0,0,0.2);"> 3443 <div style="font-family:'Marcellus',serif;font-size:11px;letter-spacing:3px;color:var(--gold);text-transform:uppercase;margin-bottom:8px;">Voice Note</div> 3444 <h3 style="font-family:'Cormorant Garamond',serif;font-size:24px;color:var(--plum-deep);font-weight:400;margin-bottom:4px;" id="vr-title">Speak from the heart</h3> 3445 <p id="vr-subtitle" style="font-family:'Cormorant Garamond',serif;font-style:italic;font-size:14px;color:var(--plum);margin-bottom:24px;">Tap the microphone to begin — up to 5 minutes</p> 3446 3447 <button id="vr-mic-btn" onclick="toggleRecording()" aria-label="Start or stop recording" style="width:110px;height:110px;border-radius:50%;background:var(--berry);color:var(--gold-light);border:3px solid var(--gold);font-size:44px;cursor:pointer;transition:all 0.3s;display:inline-flex;align-items:center;justify-content:center;"> 3448 <span id="vr-mic-icon">🎤</span> 3449 </button> 3450 3451 <div id="vr-timer" style="font-family:'Marcellus',serif;font-size:18px;letter-spacing:2px;color:var(--berry);margin-top:22px;">0:00</div> 3452 <div id="vr-hint" style="font-family:'Cormorant Garamond',serif;font-style:italic;font-size:13px;color:var(--plum);margin-top:8px;opacity:0.8;">5 minutes maximum</div> 3453 3454 <!-- Preview section (hidden during recording) --> 3455 <div id="vr-preview" style="display:none;margin-top:22px;"> 3456 <audio id="vr-playback" controls style="width:100%;height:40px;"></audio> 3457 </div> 3458 3459 <div style="margin-top:28px;display:flex;gap:10px;justify-content:center;flex-wrap:wrap;"> 3460 <button id="vr-cancel" onclick="closeVoiceRecorder()" style="padding:10px 22px;background:white;color:var(--plum);border:1px solid var(--gold);border-radius:22px;font-family:'Marcellus',serif;font-size:10px;letter-spacing:2px;text-transform:uppercase;cursor:pointer;">Cancel</button> 3461 <button id="vr-rerecord" onclick="resetVoiceRecorder()" style="display:none;padding:10px 22px;background:white;color:var(--plum);border:1px solid var(--gold);border-radius:22px;font-family:'Marcellus',serif;font-size:10px;letter-spacing:2px;text-transform:uppercase;cursor:pointer;">Re-record</button> 3462 <button id="vr-use" onclick="attachVoiceNote()" style="display:none;padding:10px 22px;background:var(--berry);color:white;border:none;border-radius:22px;font-family:'Marcellus',serif;font-size:10px;letter-spacing:2px;text-transform:uppercase;cursor:pointer;">Use This Recording</button> 3463 </div> 3464 </div> 3465 </div> 3466 3467 <!-- Filter & Search Toolbar --> 3468 <div class="feed-toolbar"> 3469 <div class="feed-filters"> 3470 <button class="feed-filter-btn active" data-filter="all" onclick="filterFeed('all')">All</button> 3471 <button class="feed-filter-btn" data-filter="announcement" onclick="filterFeed('announcement')">Announcements</button> 3472 <button class="feed-filter-btn" data-filter="teaching" onclick="filterFeed('teaching')">Teachings</button> 3473 <button class="feed-filter-btn" data-filter="prompt" onclick="filterFeed('prompt')">Prompts</button> 3474 <button class="feed-filter-btn" data-filter="reflection" onclick="filterFeed('reflection')">Reflections</button> 3475 <button class="feed-filter-btn" data-filter="hidden" onclick="filterFeed('hidden')" id="feed-hidden-btn" style="display:none;border-style:dashed;">Hidden</button> 3476 </div> 3477 <div class="feed-search">
3478 <span class="feed-search-icon">⚲</span> 3479 <input type="text" class="feed-search-input" id="feed-search-input" placeholder="Search posts..." oninput="searchFeed(this.value)"> 3480 <button class="feed-search-clear" id="feed-search-clear" onclick="clearSearch()">×</button> 3481 </div> 3482 </div> 3483 3484 <!-- One-time legacy audio converter (teacher only, auto-hidden) --> 3485 <div id="legacy-audio-banner" style="display:none;margin:0 0 18px;padding:16px 18px;background:linear-gradient(135deg,var(--cream-warm) 0%,rgba(197,165,90,0.12) 100%);border:1px solid rgba(197,165,90,0.35);border-left:4px solid var(--berry);border-radius:12px;"> 3486 <div style="font-family:'Marcellus',serif;font-size:10px;letter-spacing:2px;text-transform:uppercase;color:var(--berry);margin-bottom:6px;">Voice Note Upgrade</div> 3487 <div id="legacy-audio-text" style="font-family:'Lora',serif;font-size:13.5px;color:var(--charcoal);line-height:1.6;margin-bottom:12px;">Some older voice notes use a format that doesn’t play on iPhones. Convert them to MP3 so every sister can listen on any device.</div> 3488 <button id="legacy-audio-btn" onclick="convertLegacyVoiceNotes()" style="font-family:'Marcellus',serif;font-size:11px;letter-spacing:2px;text-transform:uppercase;color:white;background:var(--berry);border:none;padding:10px 22px;border-radius:8px;cursor:pointer;">✦ Convert Legacy Voice Notes</button> 3489 </div> 3490 3491 <!-- Feed --> 3492 <div id="feed-container"> 3493 <div class="loading"> 3494 <div class="loading-dot"></div> 3495 <div class="loading-dot"></div> 3496 <div class="loading-dot"></div> 3497 </div> 3498 </div> 3499 3500 </div> 3501 </div> 3502 3503 <!-- ===== ABOUT VIEW ===== --> 3504 <div class="view" id="view-about"> 3505 <div class="content-body"> 3506 <p class="about-welcome"> 3507 <em>May we release the endless pursuit and return to what is already luminous and available to the welcoming heart.</em> 3508 </p> 3509 3510 <div class="about-divider"><span>♦︎</span></div> 3511 3512 <div class="pillar-card"> 3513 <div class="pillar-number">Pillar One</div> 3514 <div class="pillar-title">Embodied Devotion & Presence</div> 3515 <p class="pillar-body">Slowing down as a primary orientation. Cultivating presence through the body rather than the mind. Softening control and over-efforting. Practicing surrender without collapse. Returning to what is actually happening now — and trusting that it is enough. Devotion, beauty, and sensuality as ways of perceiving and inhabiting life.</p> 3516 <div class="pillar-movement">Core movement: From thinking — into sensing, feeling, and allowing.</div> 3517 </div> 3518 3519 <div class="pillar-card"> 3520 <div class="pillar-number">Pillar Two</div> 3521 <div class="pillar-title">The Sovereign Body — Let Us Nourish & Support Her</div> 3522 <p class="pillar-body">The body is the foundation of everything we build here. Before we can lead, sense, or transmit — we must inhabit. We will approach the body from every dimension: emotional awareness, energetic attunement, spiritual practice, <em>and</em> the practical, tangible steps that most programs overlook — bloodwork, nutrition, sleep architecture, menstrual cycle awareness, hormonal stages, movement as practice, and the daily rhythms that keep us rooted, vital, and truly alive.</p> 3523 <div class="pillar-movement">Core foundation: The body as home, instrument, and teacher — honored in full.</div> 3524 </div> 3525 3526 <div class="pillar-card"> 3527 <div class="pillar-number">Pillar Three</div> 3528 <div class="pillar-title">Internal Leadership & Innate Integrity</div> 3529 <p class="pillar-body">The central thread that runs through everything. Feeling, knowing, and acting in alignment. Trusting perception and inner authority. Speaking clearly, directly, and in the moment. Leading with grace, devotion, and precision. Confidence in choosing direction. Stability in leadership while being witnessed.</p> 3530 <div class="pillar-movement">This is the thread that weaves through it all. We will honor it always.</div> 3531 </div> 3532 3533 <div class="pillar-card"> 3534 <div class="pillar-number">Pillar Four</div> 3535 <div class="pillar-title">Facilitation Embodiment & Attunement</div> 3536 <p class="pillar-body">Sensing subtle energy in self and others. Reading what is present beneath words. Tracking shifts, openings, and contractions in real time — and developing the precision to name what is actually happening. Knowing when to intervene and when to allow. Facilitating without over-controlling or disappearing.</p> 3537 <div class="pillar-movement">Core practice: Perceive it. Trust it. Name it. Lead from there.</div> 3538 </div> 3539 3540 <div class="pillar-card"> 3541 <div class="pillar-number">Pillar Five</div> 3542 <div class="pillar-title">Power, Expression & Sacred Energy</div> 3543 <p class="pillar-body">Connecting with sensual and sexual life force as creative energy — and learning to wield it with integrity. Expanding the range of expression: playfulness, sensuality, raw primal energy, direct transmission. Opening the vocal channel. Feeling safe being seen in full power.</p> 3544 <div class="pillar-movement">Core invitation: Claim the full spectrum of who you are.</div> 3545 </div> 3546 3547 <div class="pillar-card"> 3548 <div class="pillar-number">Pillar Six</div> 3549 <div class="pillar-title">Consciousness Expansion & The Unknown</div> 3550 <p class="pillar-body">Accessing what is not yet known through embodied awareness. Expanding beyond current identity and limitation. Developing trust in subtle perception, inner knowing, and non-linear intelligence. Staying present when clarity is not immediate — not rushing to resolve, fix, or define.</p> 3551 <div class="pillar-movement">Core capacity: Deep inner listening. Trust in emergence.</div> 3552 </div> 3553 3554 <div class="about-divider"><span>♦︎</span></div> 3555 3556 <div class="structure-card"> 3557 <div class="structure-heading" style="color:var(--berry);font-size:12px;font-weight:600;letter-spacing:5px;">June 2026 – January 2027</div> 3558 <div class="structure-title">The Container</div> 3559 <p style="margin-bottom:20px;font-family:'Cormorant Garamond',serif;font-style:italic;font-size:16px;line-height:1.7;text-align:center;color:var(--plum-deep);font-weight:600;">An 8-month journey<br>Held with intentional spaciousness — honoring the principle that slow is the new way.</p> 3560 3561 <div class="about-divider"><span>♦︎</span></div> 3562 3563 <div class="structure-item"> 3564 <div class="structure-icon">♦︎</div> 3565 <div> 3566 <div class="structure-item-title">Group Immersions</div> 3567 <p class="structure-item-text" style="margin-bottom:2px;"><strong>3 Calls per Month · 3 Hours Each</strong></p> 3568 <p class="structure-item-text" style="margin-bottom:2px;">Teaching Call × 1</p> 3569 <p class="structure-item-text" style="margin-bottom:8px;">Practice Calls × 2</p> 3570 <p class="structure-item-text">Where your personal evolution and professional self deepen side by side—informing and shaping one another within a powerful and creative field of other women doing the same.</p> 3571 </div> 3572 </div> 3573 3574 <div class="about-divider"><span>♦︎</span></div> 3575 3576 <div class="structure-item"> 3577 <div class="structure-icon">♦︎</div> 3578 <div> 3579 <div class="structure-item-title">Private Mentorship</div> 3580 <p class="structure-item-text" style="margin-bottom:2px;"><strong>8 Private Sessions × 1 Hour Each</strong></p> 3581 <p class="structure-item-text" style="margin-bottom:8px;">Begins in July through the end of February.</p> 3582 <p class="structure-item-text" style="margin-bottom:8px;">You may use these sessions at any cadence that suits you within the 8-month container. This is your dedicated space to go deeper, receive guidance, and be met in direct one-on-one support with Sumir.</p> 3583 <p class="structure-item-text" style="font-size:11px;opacity:0.75;font-style:italic;">Please note that if you miss your session without previously rescheduling or are more than 10 minutes late without notifying Sumir at least 1 hour prior, the session is considered forfeited. Sumir has a full schedule as well, and her time is sacred, as is yours.</p> 3584 </div> 3585 </div> 3586 3587 <div class="about-divider"><span>♦︎</span></div> 3588 3589 <div class="structure-item"> 3590 <div class="structure-icon">♦︎</div> 3591 <div> 3592 <div class="structure-item-title">Monthly Reflection & Harvest</div> 3593 <p class="structure-item-text">At the end of each month, we will reflect, share, and harvest the abundance of what has been lived, given, and received. Upleveling our experience—and one another—through our shared experiences, personal transmissions, and wisdom.</p> </div> 3594 </div> 3595 3596 <div class="about-divider"><span>♦︎</span></div> 3597 3598 <div class="structure-item"> 3599 <div class="structure-icon">♦︎</div> 3600 <div> 3601 <div class="structure-item-title">Ceremonial Opening</div> 3602 <p class="structure-item-text">We begin together June 17, 2026 —establishing the field through presence, connection, and shared intention.</p> 3603 </div> 3604 </div> 3605 3606 <div class="about-divider"><span>♦︎</span></div> 3607 3608 <div class="structure-item"> 3609 <div class="structure-icon">♦︎</div> 3610 <div> 3611 <div class="structure-item-title">6-Day Women’s Immersion (Netherlands)</div> 3612 <p class="structure-item-text" style="margin-bottom:8px;">A fully immersive, in-person experience at New Eden. Your participation, accommodation and meals are all included. Your included event is September 15–21, 2026.</p> 3613 <p class="structure-item-text" style="font-size:11px;opacity:0.75;font-style:italic;">If you’re unable to make this event, a future event will not be provided. In the unlikely event of a pandemic or unexpected worldwide emergency, the event will be postponed for the entire group and rescheduled as soon as Sumir’s schedule and the world situation allows.</p> 3614 </div> 3615 </div> 3616 3617 <div class="about-divider"><span>♦︎</span></div> 3618 3619 <div class="structure-item"> 3620 <div class="structure-icon">♦︎</div> 3621 <div> 3622 <div class="structure-item-title">Embodied Leadership</div> 3623 <p class="structure-item-text">As a woman deepens in her own evolution, it becomes the very spark of her leadership.</p> 3624 <p class="structure-item-text" style="margin-top:8px;">It is not what she says, the training she has had or the modality she uses—it is the embodiment of her being that draws others to her. Her presence becomes the transmission.</p> 3625 </div> 3626 </div> 3627 </div> 3628 3629 <div class="leadership-card"> 3630 <div class="leadership-eyebrow">Leadership Embodied</div> 3631 <div class="leadership-title">Step Into the Seat</div> 3632 <p class="leadership-body">Step into a facilitator / teaching seat within Sumir’s live event, guiding a group of 15+ women, real participants with real expectations and desires. Embody your leadership, receive direct feedback from your peers and coaching/support from Sumir as you lead in real time.</p> 3633 </div> 3634 </div> 3635 </div> 3636 3637 <!-- ===== TEACHINGS VIEW ===== --> 3638 <div class="view" id="view-teachings"> 3639 <div class="content-body"> 3640 <!-- Filter & Search Toolbar --> 3641 <div class="feed-toolbar" id="teachings-toolbar" style="display:none;"> 3642 <div class="feed-filters"> 3643 <button class="feed-filter-btn active" data-tfilter="all" onclick="filterTeachings('all')">All</button> 3644 <button class="feed-filter-btn" data-tfilter="teaching" onclick="filterTeachings('teaching')">Teachings</button> 3645 <button class="feed-filter-btn" data-tfilter="announcement" onclick="filterTeachings('announcement')">Announcements</button> 3646 </div> 3647 <div class="feed-search">
3648 <span class="feed-search-icon">⚲</span> 3649 <input type="text" class="feed-search-input" id="teachings-search-input" placeholder="Search teachings..." oninput="searchTeachings(this.value)"> 3650 <button class="feed-search-clear" id="teachings-search-clear" onclick="clearTeachingsSearch()">×</button> 3651 </div> 3652 </div> 3653 3654 <div id="teachings-container"> 3655 <div class="loading"> 3656 <div class="loading-dot"></div> 3657 <div class="loading-dot"></div> 3658 <div class="loading-dot"></div> 3659 </div> 3660 </div> 3661 </div> 3662 </div> 3663 3664 <!-- ===== REPLAYS VIEW ===== --> 3665 <div class="view" id="view-replays"> 3666 <div class="content-body"> 3667 <!-- Teacher Upload --> 3668 <div class="replay-upload" id="replay-upload" style="display:none;"> 3669 <div class="replay-upload-label">Upload a Session Replay</div> 3670 <input type="text" class="journal-textarea" id="replay-title" placeholder="Session title (e.g. Group Call â June 12)" style="min-height:auto;padding:12px 14px;margin-bottom:10px;"> 3671 <textarea class="journal-textarea" id="replay-desc" placeholder="Brief description (optional)" style="min-height:60px;margin-bottom:10px;"></textarea> 3672 <div style="display:flex;align-items:center;gap:12px;flex-wrap:wrap;"> 3673 <label style="cursor:pointer;padding:8px 16px;border:1px solid var(--gold-light);border-radius:8px;font-family:'Marcellus',serif;font-size:9px;letter-spacing:2px;text-transform:uppercase;color:var(--plum);"> 3674 🎥 Choose Video 3675 <input type="file" accept="video/*" id="replay-video-input" onchange="handleReplayVideoSelect(this)" style="display:none;"> 3676 </label> 3677 <span id="replay-video-name" style="font-size:12px;color:var(--charcoal);opacity:0.6;"></span> 3678 <button class="journal-save" id="replay-upload-btn" onclick="uploadReplay()" style="margin-top:0;margin-left:auto;">Upload Replay</button> 3679 </div> 3680 </div> 3681 3682 <div id="replays-container"> 3683 <div class="placeholder-view" id="replays-empty"> 3684 <div class="placeholder-icon">▶</div> 3685 <div class="placeholder-title">Replays</div> 3686 <p class="placeholder-text">Session replays will appear here as they become available.</p> 3687 </div> 3688 </div> 3689 </div> 3690 </div> 3691 3692 <!-- ===== JOURNAL VIEW ===== --> 3693 <div class="view" id="view-journal"> 3694 <div class="content-body"> 3695 3696 <!-- Thoughtful Inquiries --> 3697 <div class="inquiry-banner" id="inquiry-banner"> 3698 <div class="inquiry-banner-header" onclick="document.getElementById('inquiry-banner').classList.toggle('open')"> 3699 <div> 3700 <div class="inquiry-banner-title">Thoughtful Inquiries</div> 3701 <div class="inquiry-banner-subtitle">Preparing the Field for Our Opening Ceremony — June 17, 2026</div> 3702 </div> 3703 <span class="inquiry-banner-arrow">▼</span> 3704 </div> 3705 <div class="inquiry-banner-body"> 3706 <p class="inquiry-intro">Before we gather on June 17th, you are invited to sit with the following inquiries. Perhaps you also have a few inquiries for yourself.</p> 3707 3708 <!-- Inquiry 1 --> 3709 <div class="inquiry-section"> 3710 <div class="inquiry-section-num">Inquiry One</div> 3711 <div class="inquiry-section-q">What is it that you are hoping to experience inside this Private Mentorship field?</div> 3712 <div class="inquiry-line-row"><span class="inquiry-line-label">1.</span><input class="inquiry-input" data-field="q1_1" placeholder=""></div> 3713 <div class="inquiry-line-row"><span class="inquiry-line-label">2.</span><input class="inquiry-input" data-field="q1_2" placeholder=""></div> 3714 <div class="inquiry-line-row"><span class="inquiry-line-label">3.</span><input class="inquiry-input" data-field="q1_3" placeholder=""></div> 3715 </div> 3716 3717 <div class="inquiry-divider"><span>♦︎</span></div> 3718 3719 <!-- Inquiry 2 --> 3720 <div class="inquiry-section"> 3721 <div class="inquiry-section-num">Inquiry Two</div> 3722 <div class="inquiry-section-q">What are you prepared to release and transform —
3722 to support creating and bringing forth that experience?</div> 3723 <div class="inquiry-line-row"><span class="inquiry-line-label">1.</span><input class="inquiry-input" data-field="q2_1" placeholder=""></div> 3724 <div class="inquiry-line-row"><span class="inquiry-line-label">2.</span><input class="inquiry-input" data-field="q2_2" placeholder=""></div> 3725 <div class="inquiry-line-row"><span class="inquiry-line-label">3.</span><input class="inquiry-input" data-field="q2_3" placeholder=""></div> 3726 </div> 3727 3728 <div class="inquiry-divider"><span>♦︎</span></div> 3729 3730 <!-- Inquiry 3 --> 3731 <div class="inquiry-section"> 3732 <div class="inquiry-section-num">Inquiry Three</div> 3733 <div class="inquiry-section-q">What are you prepared and looking forward to offering to this space — to co-create the most electrifying, magnetic, healing field for all?</div> 3734 <div class="inquiry-line-row"><span class="inquiry-line-label">1.</span><input class="inquiry-input" data-field="q3_1" placeholder=""></div> 3735 <div class="inquiry-line-row"><span class="inquiry-line-label">2.</span><input class="inquiry-input" data-field="q3_2" placeholder=""></div> 3736 <div class="inquiry-line-row"><span class="inquiry-line-label">3.</span><input class="inquiry-input" data-field="q3_3" placeholder=""></div> 3737 </div> 3738 3739 <div class="inquiry-divider"><span>♦︎</span></div> 3740 3741 <!-- Inquiry 4 --> 3742 <div class="inquiry-section"> 3743 <div class="inquiry-section-num">Inquiry Four</div> 3744 <div class="inquiry-section-q">What are you emotionally, energetically, and practically hoping to experience during this Private Mentorship?</div> 3745 3746 <div class="inquiry-cat-title">Emotional</div> 3747 <div class="inquiry-line-row"><span class="inquiry-line-label">1.</span><input class="inquiry-input" data-field="q4_emotional_1" placeholder=""></div> 3748 <div class="inquiry-line-row"><span class="inquiry-line-label">2.</span><input class="inquiry-input" data-field="q4_emotional_2" placeholder=""></div> 3749 <div class="inquiry-line-row"><span class="inquiry-line-label">3.</span><input class="inquiry-input" data-field="q4_emotional_3" placeholder=""></div> 3750 3751 <div class="inquiry-cat-title">Energetic</div> 3752 <div class="inquiry-line-row"><span class="inquiry-line-label">1.</span><input class="inquiry-input" data-field="q4_energetic_1" placeholder=""></div> 3753 <div class="inquiry-line-row"><span class="inquiry-line-label">2.</span><input class="inquiry-input" data-field="q4_energetic_2" placeholder=""></div> 3754 <div class="inquiry-line-row"><span class="inquiry-line-label">3.</span><input class="inquiry-input" data-field="q4_energetic_3" placeholder=""></div> 3755 3756 <div class="inquiry-cat-title">Spiritual</div> 3757 <div class="inquiry-line-row"><span class="inquiry-line-label">1.</span><input class="inquiry-input" data-field="q4_spiritual_1" placeholder=""></div> 3758 <div class="inquiry-line-row"><span class="inquiry-line-label">2.</span><input class="inquiry-input" data-field="q4_spiritual_2" placeholder=""></div> 3759 <div class="inquiry-line-row"><span class="inquiry-line-label">3.</span><input class="inquiry-input" data-field="q4_spiritual_3" placeholder=""></div> 3760 3761 <div class="inquiry-cat-title">Practical</div> 3762 <div class="inquiry-line-row"><span class="inquiry-line-label">1.</span><input class="inquiry-input" data-field="q4_practical_1" placeholder=""></div> 3763 <div class="inquiry-line-row"><span class="inquiry-line-label">2.</span><input class="inquiry-input" data-field="q4_practical_2" placeholder=""></div> 3764 <div class="inquiry-line-row"><span class="inquiry-line-label">3.</span><input class="inquiry-input" data-field="q4_practical_3" placeholder=""></div> 3765 3766 <div class="inquiry-cat-title">Physical</div> 3767 <div class="inquiry-line-row"><span class="inquiry-line-label">1.</span><input class="inquiry-input" data-field="q4_physical_1" placeholder=""></div> 3768 <div class="inquiry-line-row"><span class="inquiry-line-label">2.</span><input class="inquiry-input" data-field="q4_physical_2" placeholder=""></div> 3769 <div class="inquiry-line-row"><span class="inquiry-line-label">3.</span><input class="inquiry-input" data-field="q4_physical_3" placeholder=""></div> 3770 3771 <div class="inquiry-cat-title">Facilitator Experience</div> 3772 <div class="inquiry-line-row"><span class="inquiry-line-label">1.</span><input class="inquiry-input" data-field="q4_facilitator_1" placeholder=""></div> 3773 <div class="inquiry-line-row"><span class="inquiry-line-label">2.</span><input class="inquiry-input" data-field="q4_facilitator_2" placeholder=""></div> 3774 <div class="inquiry-line-row"><span class="inquiry-line-label">3.</span><input class="inquiry-input" data-field="q4_facilitator_3" placeholder=""></div> 3775 </div> 3776 3777 <div class="inquiry-divider"><span>♦︎</span></div> 3778 3779 <!-- Inquiry 5 --> 3780 <div class="inquiry-section"> 3781 <div class="inquiry-section-num">Inquiry Five</div> 3782 <div class="inquiry-section-q">What are you most excited about?</div> 3783 <div class="inquiry-line-row"><span class="inquiry-line-label">1.</span><input class="inquiry-input" data-field="q5_excited_1" placeholder=""></div> 3784 <div class="inquiry-line-row"><span class="inquiry-line-label">2.</span><input class="inquiry-input" data-field="q5_excited_2" placeholder=""></div> 3785 <div class="inquiry-line-row"><span class="inquiry-line-label">3.</span><input class="inquiry-input" data-field="q5_excited_3" placeholder=""></div> 3786 3787 <div class="inquiry-section-q" style="margin-top:20px;">What are you most fearful or nervous about?</div> 3788 <div class="inquiry-line-row"><span class="inquiry-line-label">1.</span><input class="inquiry-input" data-field="q5_fearful_1" placeholder=""></div> 3789 <div class="inquiry-line-row"><span class="inquiry-line-label">2.</span><input class="inquiry-input" data-field="q5_fearful_2" placeholder=""></div> 3790 <div class="inquiry-line-row"><span class="inquiry-line-label">3.</span><input class="inquiry-input" data-field="q5_fearful_3" placeholder=""></div> 3791 </div> 3792 3793 <!-- Save --> 3794 <div class="inquiry-save-row">
3795 <span class="inquiry-saved-msg" id="inquiry-saved-msg">Saved as Journal Entry #1</span> 3796 <button class="inquiry-save-btn" onclick="saveInquiryResponses()">Save My Responses</button> 3797 </div> 3798 </div> 3799 </div> 3800 3801 <div id="journal-prompts"></div> 3802 3803 <div class="journal-compose"> 3804 <div class="journal-compose-label">Write in Your Journal</div> 3805 <textarea class="journal-textarea" id="journal-body" placeholder="What is alive in you right now..."></textarea> 3806 <div style="display:flex;align-items:center;justify-content:space-between;margin-top:10px;flex-wrap:wrap;gap:10px;"> 3807 <div class="journal-visibility-toggle"> 3808 <button class="journal-vis-btn active" id="vis-private" onclick="setJournalVisibility('private')">🔒 Private</button> 3809 <button class="journal-vis-btn" id="vis-public" onclick="setJournalVisibility('public')">👁 Visible to Sumir</button> 3810 </div> 3811 <div style="display:flex;gap:8px;"> 3812 <button class="journal-save journal-save-draft" onclick="saveJournalEntry(true)" title="Save without publishing">Save as Draft</button> 3813 <button class="journal-save" onclick="saveJournalEntry(false)">Save Entry</button> 3814 </div> 3815 </div> 3816 <div id="editing-draft-banner" style="display:none;margin-top:10px;padding:8px 12px;background:rgba(197,165,90,0.1);border:1px dashed var(--gold);border-radius:6px;font-family:'Lora',serif;font-size:12px;color:var(--plum);"> 3817 <span>✎ Editing draft — <button onclick="cancelDraftEdit()" style="background:none;border:none;color:var(--berry);text-decoration:underline;cursor:pointer;font-family:inherit;font-size:inherit;padding:0;">cancel</button></span> 3818 </div> 3819 </div> 3820 3821 <div id="journal-toolbar" style="display:none;align-items:center;gap:12px;margin-bottom:14px;flex-wrap:wrap;"> 3822 <div style="position:relative;flex:1;
3822min-width:200px;"> 3823 <input type="text" id="journal-search-input" placeholder="Search your journal..." oninput="filterJournalEntries(this.value)" style="width:100%;padding:10px 36px 10px 14px;border:1px solid rgba(197,165,90,0.3);border-radius:18px;background:white;font-family:'Lora',serif;font-size:13px;color:var(--charcoal);outline:none;"> 3824 <button id="journal-search-clear" onclick="clearJournalSearch()" style="position:absolute;right:10px;top:50%;transform:translateY(-50%);background:none;border:none;color:var(--gold);font-size:16px;cursor:pointer;display:none;line-height:1;padding:4px 6px;">×</button> 3825 </div> 3826 <div id="journal-print-trigger" style="display:flex;gap:8px;"> 3827 <button class="journal-print-btn" onclick="printJournal()" title="Print or Save as PDF"> 3828 <span class="print-icon">✎</span> Print / Save as PDF 3829 </button> 3830 </div> 3831 </div> 3832 3833 <div id="journal-entries"></div> 3834 </div> 3835 </div> 3836 3837 <!-- ===== SCHEDULE VIEW ===== --> 3838 <div class="view" id="view-schedule"> 3839 <div class="content-body"> 3840 <div style="max-width:600px;margin:0 auto;"> 3841 3842 <div style="background:white;border:1px solid rgba(197,165,90,0.25);border-radius:14px;padding:32px 28px;text-align:center;margin-bottom:30px;box-shadow:0 2px 12px rgba(197,165,90,0.08);"> 3843 <p style="margin-bottom:14px;"><a href="https://us06web.zoom.us/j/7772877728" target="_blank" style="font-family:'Marcellus',serif;font-size:11px;letter-spacing:2px;text-transform:uppercase;color:white;background:var(--plum);text-decoration:none;border:none;padding:12px 30px;border-radius:8px;display:inline-block;transition:all 0.3s;">✦ Join Zoom Call</a></p> 3844 <p style="font-family:'Lora',serif;font-size:11px;color:var(--charcoal);opacity:0.75;word-break:break-all;user-select:all;cursor:text;">https://us06web.zoom.us/j/7772877728</p> 3845 </div> 3846 3847 <div class="about-divider"><span>♦︎</span></div> 3848 3849 <!-- June --> 3850 <div style="margin-bottom:24px;"> 3851 <div style="font-family:'Marcellus',serif;font-size:10px;letter-spacing:3px;text-transform:uppercase;color:var(--berry);margin-bottom:10px;">June</div> 3852 <div style="display:flex;flex-direction:column;gap:8px;"> 3853 <div style="background:white;border:1px solid rgba(197,165,90,0.15);border-radius:10px;padding:14px 18px;display:flex;align-items:center;justify-content:space-between;"> 3854 <span style="font-family:'Cormorant Garamond',serif;font-size:17px;color:var(--plum-deep);">June 17 · 4pm CET</span> 3855 <span style="font-family:'Marcellus',serif;font-size:8px;letter-spacing:2px;text-transform:uppercase;color:var(--gold);">Opening Ceremony</span> 3856 </div> 3857 <div style="background:white;border:1px solid rgba(197,165,90,0.15);border-radius:10px;padding:14px 18px;display:flex;align-items:center;justify-content:space-between;"> 3858 <span style="font-family:'Cormorant Garamond',serif;font-size:17px;color:var(--plum-deep);">June 28 · 4pm CET</span> 3859 <span style="font-family:'Marcellus',serif;font-size:8px;letter-spacing:2px;text-transform:uppercase;color:var(--gold);">Teaching Call</span> 3860 </div> 3861 </div> 3862 </div> 3863 3864 <!-- July --> 3865 <div style="margin-bottom:24px;"> 3866 <div style="font-family:'Marcellus',serif;font-size:10px;letter-spacing:3px;text-transform:uppercase;color:var(--berry);margin-bottom:10px;">July</div> 3867 <div style="display:flex;flex-direction:column;gap:8px;"> 3868 <div style="background:white;border:1px solid rgba(197,165,90,0.15);border-radius:10px;padding:14px 18px;display:flex;align-items:center;justify-content:space-between;"> 3869 <span style="font-family:'Cormorant Garamond',serif;font-size:17px;color:var(--plum-deep);">July 5 · 4pm CET</span>
3870 <span style="font-family:'Marcellus',serif;font-size:8px;letter-spacing:2px;text-transform:uppercase;color:var(--gold);">Teaching Call</span> 3871 </div> 3872 <div style="background:white;border:1px solid rgba(197,165,90,0.15);border-radius:10px;padding:14px 18px;display:flex;align-items:center;justify-content:space-between;"> 3873 <span style="font-family:'Cormorant Garamond',serif;font-size:17px;color:var(--plum-deep);">July 19 · 4pm CET</span> 3874 <span style="font-family:'Marcellus',serif;font-size:8px;letter-spacing:2px;text-transform:uppercase;color:var(--gold);">Practice Call</span> 3875 </div> 3876 <div style="background:white;border:1px solid rgba(197,165,90,0.15);border-radius:10px;padding:14px 18px;display:flex;align-items:center;justify-content:space-between;"> 3877 <span style="font-family:'Cormorant Garamond',serif;font-size:17px;color:var(--plum-deep);">July 22 · 6pm CET</span> 3878 <span style="font-family:'Marcellus',serif;font-size:8px;letter-spacing:2px;text-transform:uppercase;color:var(--gold);">Practice Call</span> 3879 </div> 3880 </div> 3881 </div> 3882 3883 <!-- August --> 3884 <div style="margin-bottom:24px;"> 3885 <div style="font-family:'Marcellus',serif;font-size:10px;letter-spacing:3px;text-transform:uppercase;color:var(--berry);margin-bottom:10px;">August</div> 3886 <div style="display:flex;flex-direction:column;gap:8px;"> 3887 <div style="background:white;border:1px solid rgba(197,165,90,0.15);border-radius:10px;padding:14px 18px;display:flex;align-items:center;justify-content:space-between;"> 3888 <span style="font-family:'Cormorant Garamond',serif;font-size:17px;color:var(--plum-deep);">August 5 · 3pm CET</span> 3889 <span style="font-family:'Marcellus',serif;font-size:8px;letter-spacing:2px;text-transform:uppercase;color:var(--gold);">Teaching Call</span> 3890 </div> 3891 <div style="background:white;border:1px solid rgba(197,165,90,0.15);border-radius:10px;padding:14px 18px;display:flex;align-items:center;justify-content:space-between;"> 3892 <span style="font-family:'Cormorant Garamond',serif;font-size:17px;color:var(--plum-deep);">August 12 · 3pm CET</span> 3893 <span style="font-family:'Marcellus',serif;font-size:8px;letter-spacing:2px;text-transform:uppercase;color:var(--gold);">Practice Call</span> 3894 </div> 3895 <div style="background:white;border:1px solid rgba(197,165,90,0.15);border-radius:10px;padding:14px 18px;display:flex;align-items:center;justify-content:space-between;"> 3896 <span style="font-family:'Cormorant Garamond',serif;font-size:17px;color:var(--plum-deep);">August 19 · 6pm CET</span> 3897 <span style="font-family:'Marcellus',serif;font-size:8px;letter-spacing:2px;text-transform:uppercase;color:var(--gold);">Practice Call</span> 3898 </div> 3899 </div> 3900 </div> 3901 3902 <!-- September --> 3903 <div style="margin-bottom:24px;"> 3904 <div style="font-family:'Marcellus',serif;font-size:10px;letter-spacing:3px;text-transform:uppercase;color:var(--berry);margin-bottom:10px;">September</div> 3905 <div style="display:flex;flex-direction:column;gap:8px;"> 3906 <div style="background:white;border:1px solid rgba(197,165,90,0.15);border-radius:10px;padding:14px 18px;display:flex;align-items:center;justify-content:space-between;"> 3907 <span style="font-family:'Cormorant Garamond',serif;font-size:17px;color:var(--plum-deep);">September 6 · 4pm CET</span> 3908 <span style="font-family:'Marcellus',serif;font-size:8px;letter-spacing:2px;text-transform:uppercase;color:var(--gold);">Teaching & Event Tee Up</span> 3909 </div> 3910 <div style="background:white;border:1px solid rgba(197,165,90,0.15);border-radius:10px;padding:14px 18px;display:flex;align-items:center;justify-content:space-between;">
3911 <span style="font-family:'Cormorant Garamond',serif;font-size:17px;color:var(--plum-deep);">September 15 – 21</span> 3912 <span style="font-family:'Marcellus',serif;font-size:8px;letter-spacing:2px;text-transform:uppercase;color:var(--gold);">Live Event</span> 3913 </div> 3914 <div style="background:white;border:1px solid rgba(197,165,90,0.15);border-radius:10px;padding:14px 18px;display:flex;align-items:center;justify-content:space-between;"> 3915 <span style="font-family:'Cormorant Garamond',serif;font-size:17px;color:var(--plum-deep);">September 27 · 4pm CET</span> 3916 <span style="font-family:'Marcellus',serif;font-size:8px;letter-spacing:2px;text-transform:uppercase;color:var(--gold);">Event Follow Up</span> 3917 </div> 3918 </div> 3919 </div> 3920 3921 <!-- October --> 3922 <div style="margin-bottom:24px;"> 3923 <div style="font-family:'Marcellus',serif;font-size:10px;letter-spacing:3px;text-transform:uppercase;color:var(--berry);margin-bottom:10px;">October</div> 3924 <div style="display:flex;flex-direction:column;gap:8px;"> 3925 <div style="background:white;border:1px solid rgba(197,165,90,0.15);border-radius:10px;padding:14px 18px;display:flex;align-items:center;justify-content:space-between;"> 3926 <span style="font-family:'Cormorant Garamond',serif;font-size:17px;color:var(--plum-deep);">TBD</span> 3927 <span style="font-family:'Marcellus',serif;font-size:8px;letter-spacing:2px;text-transform:uppercase;color:var(--gold);">Teaching Call</span> 3928 </div> 3929 <div style="background:white;border:1px solid rgba(197,165,90,0.15);border-radius:10px;padding:14px 18px;display:flex;align-items:center;justify-content:space-between;"> 3930 <span style="font-family:'Cormorant Garamond',serif;font-size:17px;color:var(--plum-deep);">TBD</span> 3931 <span style="font-family:'Marcellus',serif;font-size:8px;letter-spacing:2px;text-transform:uppercase;color:var(--gold);">Practice Call 1</span> 3932 </div> 3933 <div style="background:white;border:1px solid rgba(197,165,90,0.15);border-radius:10px;padding:14px 18px;display:flex;align-items:center;justify-content:space-between;"> 3934 <span style="font-family:'Cormorant Garamond',serif;font-size:17px;color:var(--plum-deep);">TBD</span> 3935 <span style="font-family:'Marcellus',serif;font-size:8px;letter-spacing:2px;text-transform:uppercase;color:var(--gold);">Practice Call 2</span> 3936 </div> 3937 </div> 3938 </div> 3939 3940 <!-- November --> 3941 <div style="margin-bottom:24px;"> 3942 <div style="font-family:'Marcellus',serif;font-size:10px;letter-spacing:3px;text-transform:uppercase;color:var(--berry);margin-bottom:10px;">November</div> 3943 <div style="display:flex;flex-direction:column;gap:8px;"> 3944 <div style="background:white;border:1px solid rgba(197,165,90,0.15);border-radius:10px;padding:14px 18px;display:flex;align-items:center;justify-content:space-between;"> 3945 <span style="font-family:'Cormorant Garamond',serif;font-size:17px;color:var(--plum-deep);">TBD</span> 3946 <span style="font-family:'Marcellus',serif;font-size:8px;letter-spacing:2px;text-transform:uppercase;color:var(--gold);">Teaching Call</span> 3947 </div> 3948 <div style="background:white;border:1px solid rgba(197,165,90,0.15);border-radius:10px;padding:14px 18px;display:flex;align-items:center;justify-content:space-between;"> 3949 <span style="font-family:'Cormorant Garamond',serif;font-size:17px;color:var(--plum-deep);">TBD</span> 3950 <span style="font-family:'Marcellus',serif;font-size:8px;letter-spacing:2px;text-transform:uppercase;color:var(--gold);">Practice Call 1</span> 3951 </div> 3952 <div style="background:white;border:1px solid rgba(197,165,90,0.15);border-radius:10px;padding:14px 18px;display:flex;align-items:center;justify-content:space-between;">
3953 <span style="font-family:'Cormorant Garamond',serif;font-size:17px;color:var(--plum-deep);">TBD</span> 3954 <span style="font-family:'Marcellus',serif;font-size:8px;letter-spacing:2px;text-transform:uppercase;color:var(--gold);">Practice Call 2</span> 3955 </div> 3956 </div> 3957 </div> 3958 3959 <div class="about-divider"><span>♦︎</span></div> 3960 3961 <!-- Integration Period â December --> 3962 <div style="background:var(--cream-warm);border-radius:14px;border:1px solid rgba(197,165,90,0.15);padding:28px 24px;text-align:center;margin-bottom:24px;"> 3963 <div style="font-family:'Marcellus',serif;font-size:10px;letter-spacing:3px;text-transform:uppercase;color:var(--gold);margin-bottom:6px;">Integration Period</div> 3964 <div style="font-family:'Cormorant Garamond',serif;font-size:22px;color:var(--plum-deep);margin-bottom:8px;">December</div> 3965 <p style="font-family:'Cormorant Garamond',serif;font-style:italic;font-size:15px;color:var(--plum);opacity:0.8;">A dedicated month for rest, integration, and embodiment.<br>No group calls.</p> 3966 </div> 3967 3968 <div class="about-divider"><span>♦︎</span></div> 3969 3970 <!-- January --> 3971 <div style="margin-bottom:24px;"> 3972 <div style="font-family:'Marcellus',serif;font-size:10px;letter-spacing:3px;text-transform:uppercase;color:var(--berry);margin-bottom:10px;">January</div> 3973 <div style="display:flex;flex-direction:column;gap:8px;"> 3974 <div style="background:white;border:1px solid rgba(197,165,90,0.15);border-radius:10px;padding:14px 18px;display:flex;align-items:center;justify-content:space-between;"> 3975 <span style="font-family:'Cormorant Garamond',serif;font-size:17px;color:var(--plum-deep);">TBD</span> 3976 <span style="font-family:'Marcellus',serif;font-size:8px;letter-spacing:2px;text-transform:uppercase;color:var(--gold);">Teaching Call</span> 3977 </div> 3978 <div style="background:white;border:1px solid rgba(197,165,90,0.15);border-radius:10px;padding:14px 18px;display:flex;align-items:center;justify-content:space-between;"> 3979 <span style="font-family:'Cormorant Garamond',serif;font-size:17px;color:var(--plum-deep);">TBD</span> 3980 <span style="font-family:'Marcellus',serif;font-size:8px;letter-spacing:2px;text-transform:uppercase;color:var(--gold);">Practice Call 1</span> 3981 </div> 3982 <div style="background:white;border:1px solid rgba(197,165,90,0.15);border-radius:10px;padding:14px 18px;display:flex;align-items:center;justify-content:space-between;"> 3983 <span style="font-family:'Cormorant Garamond',serif;font-size:17px;color:var(--plum-deep);">TBD</span> 3984 <span style="font-family:'Marcellus',serif;font-size:8px;letter-spacing:2px;text-transform:uppercase;color:var(--gold);">Practice Call 2</span> 3985 </div> 3986 </div> 3987 </div> 3988 3989 <!-- February --> 3990 <div style="margin-bottom:24px;"> 3991 <div style="font-family:'Marcellus',serif;font-size:10px;letter-spacing:3px;text-transform:uppercase;color:var(--berry);margin-bottom:10px;">February</div> 3992 <div style="display:flex;flex-direction:column;gap:8px;"> 3993 <div style="background:white;border:1px solid rgba(197,165,90,0.15);border-radius:10px;padding:14px 18px;display:flex;align-items:center;justify-content:space-between;"> 3994 <span style="font-family:'Cormorant Garamond',serif;font-size:17px;color:var(--plum-deep);">TBD</span> 3995 <span style="font-family:'Marcellus',serif;font-size:8px;letter-spacing:2px;text-transform:uppercase;color:var(--gold);">Teaching Call</span> 3996 </div> 3997 <div style="background:white;border:1px solid rgba(197,165,90,0.15);border-radius:10px;padding:14px 18px;display:flex;align-items:center;justify-content:space-between;">
3998 <span style="font-family:'Cormorant Garamond',serif;font-size:17px;color:var(--plum-deep);">TBD</span> 3999 <span style="font-family:'Marcellus',serif;font-size:8px;letter-spacing:2px;text-transform:uppercase;color:var(--gold);">Practice Call 1</span> 4000 </div> 4001 <div style="background:white;border:1px solid rgba(148,14,75,0.12);border-radius:10px;padding:14px 18px;display:flex;align-items:center;justify-content:space-between;"> 4002 <span style="font-family:'Cormorant Garamond',serif;font-size:17px;color:var(--plum-deep);">TBD</span> 4003 <span style="font-family:'Marcellus',serif;font-size:8px;letter-spacing:2px;text-transform:uppercase;color:var(--berry);">Practice Call 2 / Final Call ✦</span> 4004 </div> 4005 </div> 4006 </div> 4007 4008 </div> 4009 </div> 4010 </div> 4011 4012 <!-- ===== TOOLS VIEW ===== --> 4013 <div class="view" id="view-tools"> 4014 <div class="content-body"> 4015 <p class="about-welcome" style="margin-bottom:24px;"> 4016 <em>Simple guides to help you make the most of this space.</em> 4017 </p> 4018 4019 <div class="tools-grid"> 4020 4021 <!-- Tutorial: Thoughtful Inquiries --> 4022 <div class="tool-card" onclick="this.classList.toggle('open')"> 4023 <div class="tool-card-header"> 4024 <div class="tool-card-header-left"> 4025 <div class="tool-card-icon">✿</div> 4026 <div> 4027 <div class="tool-card-title">Completing Your Thoughtful Inquiries</div> 4028 <div class="tool-card-subtitle">Your first journal entry & preparation for the Opening Ceremony</div> 4029 </div> 4030 </div> 4031 <div class="tool-card-arrow">▶</div> 4032 </div> 4033 <div class="tool-card-content"> 4034 4035 <div class="guide-note" style="border-left-color:var(--berry);"> 4036 <strong>What are the Thoughtful Inquiries?</strong> These are five reflective inquiries designed to help you begin entering the field we will co-create together. Your responses become your very first journal entry — and they’re always available for you to revisit and update as you evolve through this experience. 4037 </div> 4038 4039 <div class="guide-section"> 4040 <div class="guide-section-title">Step by Step</div> 4041 <div class="guide-steps"> 4042 <div class="guide-step"> 4043 <div class="guide-step-num">1</div> 4044 <div class="guide-step-text">Tap <strong>Journal</strong> in the sidebar.</div> 4045 </div> 4046 <div class="guide-step"> 4047 <div class="guide-step-num">2</div> 4048 <div class="guide-step-text">At the very top of the page, you’ll see the <strong>Thoughtful Inquiries</strong> card. Tap it to expand.</div> 4049 </div> 4050 <div class="guide-step"> 4051 <div class="guide-step-num">3</div> 4052 <div class="guide-step-text">Read each inquiry slowly. There are five in total, each with space for your responses.</div> 4053 </div> 4054 <div class="guide-step"> 4055 <div class="guide-step-num">4</div> 4056 <div class="guide-step-text">Fill in your responses on the lines provided. You don’t need to complete them all at once — you can return and update them anytime.</div> 4057 </div> 4058 <div class="guide-step"> 4059 <div class="guide-step-num">5</div> 4060 <div class="guide-step-text">When you’re ready, tap <strong>Save My Responses</strong> at the bottom. Your answers are saved as Journal Entry #1.</div> 4061 </div> 4062 <div class="guide-step"> 4063 <div class="guide-step-num">6</div>
4064 <div class="guide-step-text">Come prepared to share your responses at our <strong>Opening Ceremony on June 17th</strong>.</div> 4065 </div> 4066 </div> 4067 </div> 4068 4069 <div class="guide-note"> 4070 <strong>A few things to know:</strong> 4071 <ul style="padding-left:20px;line-height:1.8;margin-top:8px;margin-bottom:0;"> 4072 <li>Your Thoughtful Inquiry responses will be <strong>automatically visible to Sumir</strong>, so she can prepare and hold the field with and for you.</li> 4073 <li>All other Journal Entries moving forward will default to <strong>private</strong> (your eyes only) unless you specifically choose to share them with Sumir via the option provided on each journal entry.</li> 4074 <li>You can update your inquiry responses any time up to <strong>June 17th</strong>, at which point we will seal them and let them be as they are — visible but unchangeable — until the end of our experience together.</li> 4075 </ul> 4076 </div> 4077 4078 </div> 4079 </div> 4080 4081 <!-- Tutorial: Golden Path Teachings Access --> 4082 <div class="tool-card" onclick="this.classList.toggle('open')"> 4083 <div class="tool-card-header"> 4084 <div class="tool-card-header-left"> 4085 <div class="tool-card-icon">✨</div> 4086 <div> 4087 <div class="tool-card-title">Golden Path Teachings Access</div> 4088 <div class="tool-card-subtitle">How to access the full Golden Path teaching library</div> 4089 </div> 4090 </div> 4091 <div class="tool-card-arrow">▶</div> 4092 </div> 4093 <div class="tool-card-content"> 4094 4095 <div class="guide-note" style="border-left-color:var(--berry);"> 4096 <strong>What is the Golden Path Library?</strong> As a Private Mentorship member, you have full access to the Golden Path teaching collection — a library of teachings, practices, and sacred resources. This is a separate portal that uses the same access code as your Private Mentorship. 4097 </div> 4098 4099 <div class="guide-section"> 4100 <div class="guide-section-title">Step by Step</div> 4101 <div class="guide-steps"> 4102 <div class="guide-step"> 4103 <div class="guide-step-num">1</div> 4104 <div class="guide-step-text">Go to <strong>femininerising.co</strong> in your browser.</div> 4105 </div> 4106 <div class="guide-step"> 4107 <div class="guide-step-num">2</div> 4108 <div class="guide-step-text">On the Portal page, find and click the <strong>Golden Path Library</strong> tile.</div> 4109 </div> 4110 <div class="guide-step"> 4111 <div class="guide-step-num">3</div> 4112 <div class="guide-step-text">When prompted, enter the access code: <strong style="color:var(--berry);letter-spacing:1px;">embodied2026</strong></div> 4113 </div> 4114 <div class="guide-step"> 4115 <div class="guide-step-num">4</div> 4116 <div class="guide-step-text">You’re in. Browse the full library of teachings, bookmark your favorites, and return anytime.</div> 4117 </div> 4118 </div> 4119 </div> 4120 4121 <div class="guide-note"> 4122 <strong>Good to know:</strong> 4123 <ul style="padding-left:20px;line-height:1.8;margin-top:8px;margin-bottom:0;"> 4124 <li>This is the same access code you use for your Private Mentorship portal.</li> 4125 <li>The Golden Path Library is available to you throughout the duration of your mentorship.</li> 4126 <li>If you have any trouble accessing the library, contact <strong>[email protected]</strong>.</li> 4127 </ul> 4128 </div> 4129 4130 </div> 4131 </div> 4132 4133 <!-- Tutorial 1: Video Posts --> 4134 <div class="tool-card" onclick="this.classList.toggle('open')"> 4135 <div class="tool-card-header"> 4136 <div class="tool-card-header-left"> 4137 <div class="tool-card-icon">🎥</div> 4138 <div> 4139 <div class="tool-card-title">How to Share a Video Post</div> 4140 <div class="tool-card-subtitle">Record & share video in the Community</div> 4141 </div> 4142 </div> 4143 <div class="tool-card-arrow">▶</div> 4144 </div> 4145 <div class="tool-card-content"> 4146
4147 <div class="guide-note"> 4148 <strong>Good news:</strong> The steps are the same whether you’re on iPhone or Android. Wherever you see your camera roll or file picker, you can also record a new video on the spot. 4149 </div> 4150 4151 <div class="guide-section"> 4152 <div class="guide-section-title">From Your Phone</div> 4153 4154 <div class="guide-step"> 4155 <div class="guide-step-num">1</div> 4156 <div class="guide-step-text">Tap <strong>Community</strong> in the sidebar.</div> 4157 </div> 4158 4159 <div class="guide-step"> 4160 <div class="guide-step-num">2</div> 4161 <div class="guide-step-text">Tap the <strong>“Share something with the community...”</strong> bar at the top. The composer will expand.</div> 4162 </div> 4163 4164 <div class="guide-step"> 4165 <div class="guide-step-num">3</div> 4166 <div class="guide-step-text">Write your message in the text area. Add a title if you’d like (optional).</div> 4167 </div> 4168 4169 <div class="guide-step"> 4170 <div class="guide-step-num">4</div> 4171 <div class="guide-step-text">Tap the <strong>🎥 Photo / Video</strong> button at the bottom.</div> 4172 </div> 4173 4174 <div class="guide-step"> 4175 <div class="guide-step-num">5</div> 4176 <div class="guide-step-text">Your phone will ask you to <strong>choose a file</strong>. You can either:<br> 4177 • Select an existing video from your camera roll<br> 4178 • Choose <strong>“Camera”</strong> to record a new video right there</div> 4179 </div> 4180 4181 <div class="guide-step"> 4182 <div class="guide-step-num">6</div> 4183 <div class="guide-step-text">Once selected, you’ll see a <strong>preview</strong> of your video in the composer. You can tap the <strong>×</strong> to remove it if you change your mind.</div> 4184 </div> 4185 4186 <div class="guide-step"> 4187 <div class="guide-step-num">7</div> 4188 <div class="guide-step-text">When you’re ready, tap <strong>Share</strong>. The video will upload and your post will appear in the feed.</div> 4189 </div> 4190 </div> 4191 4192 <div class="guide-divider"></div> 4193 4194 <div class="guide-section"> 4195 <div class="guide-section-title">From Your Computer</div> 4196 4197 <div class="guide-step"> 4198 <div class="guide-step-num">1</div> 4199 <div class="guide-step-text">Click <strong>Community</strong> in the sidebar.</div> 4200 </div> 4201 4202 <div class="guide-step"> 4203 <div class="guide-step-num">2</div> 4204 <div class="guide-step-text">Click the <strong>“Share something with the community...”</strong> bar. The composer will expand.</div> 4205 </div> 4206 4207 <div class="guide-step"> 4208 <div class="guide-step-num">3</div> 4209 <div class="guide-step-text">Write your message and click the <strong>🎥 Photo / Video</strong> button.</div> 4210 </div> 4211 4212 <div class="guide-step"> 4213 <div class="guide-step-num">4</div> 4214 <div class="guide-step-text">A file picker will open. Navigate to where your video is saved and select it.</div> 4215 </div> 4216 4217 <div class="guide-step"> 4218 <div class="guide-step-num">5</div> 4219 <div class="guide-step-text">You’ll see a preview. When you’re ready, click <strong>Share</strong>.</div> 4220 </div> 4221 </div> 4222 4223 <div class="guide-divider"></div> 4224
4225 <div class="guide-note"> 4226 <strong>🔒 Privacy note:</strong> All videos shared in this space are protected. They cannot be downloaded or saved by other members. This is a safe space to be seen. 4227 </div> 4228 4229 <div class="guide-note" style="border-left-color:var(--berry);"> 4230 <strong>Tip:</strong> Shorter videos (under 2 minutes) upload fastest. If your video is long, give it a moment — you’ll see “Sharing...” while it uploads. 4231 </div> 4232 4233 </div> 4234 </div> 4235 4236 <!-- Tutorial: Voice Notes --> 4237 <div class="tool-card" onclick="this.classList.toggle('open')"> 4238 <div class="tool-card-header"> 4239 <div class="tool-card-header-left"> 4240 <div class="tool-card-icon">🎤</div> 4241 <div> 4242 <div class="tool-card-title">How to Post a Voice Note</div> 4243 <div class="tool-card-subtitle">Share a spoken reflection in the Community (up to 5 minutes)</div> 4244 </div> 4245 </div> 4246 <div class="tool-card-arrow">▶</div> 4247 </div> 4248 <div class="tool-card-content"> 4249 4250 <div class="guide-note"> 4251 Voice notes are a beautiful way to share when you’d rather speak than type. Your phone or computer will ask permission to use your microphone the first time — just tap <strong>Allow</strong>. 4252 </div> 4253 4254 <div class="guide-section"> 4255 <div class="guide-section-title">Step by Step</div> 4256 4257 <div class="guide-step"> 4258 <div class="guide-step-num">1</div> 4259 <div class="guide-step-text">Tap <strong>Community</strong> in the sidebar.</div> 4260 </div> 4261 4262 <div class="guide-step"> 4263 <div class="guide-step-num">2</div> 4264 <div class="guide-step-text">Tap the <strong>“Share something with the community…”</strong> bar at the top. The composer will expand.</div> 4265 </div> 4266 4267 <div class="guide-step"> 4268 <div class="guide-step-num">3</div> 4269 <div class="guide-step-text">Tap the <strong>🎤 Voice Note</strong> button at the bottom of the composer. A recording window will open.</div> 4270 </div> 4271 4272 <div class="guide-step"> 4273 <div class="guide-step-num">4</div> 4274 <div class="guide-step-text">The first time you do this, your browser will ask to use your microphone. Tap <strong>Allow</strong>.</div> 4275 </div> 4276 4277 <div class="guide-step"> 4278 <div class="guide-step-num">5</div> 4279 <div class="guide-step-text">Tap the big <strong>berry microphone button</strong> to start recording. You’ll see a timer counting up.</div> 4280 </div> 4281 4282 <div class="guide-step"> 4283 <div class="guide-step-num">6</div> 4284 <div class="guide-step-text">When you’re finished, tap the button again to stop. (It auto-stops at 5 minutes.)</div> 4285 </div> 4286 4287 <div class="guide-step"> 4288 <div class="guide-step-num">7</div> 4289 <div class="guide-step-text">Listen back using the little audio player. Don’t love it? Tap <strong>Re-record</strong> to try again.</div> 4290 </div> 4291 4292 <div class="guide-step"> 4293 <div class="guide-step-num">8</div> 4294 <div class="guide-step-text">When you’re happy, tap <strong>Use This Recording</strong>. It will attach to your post.</div> 4295 </div> 4296 4297 <div class="guide-step"> 4298 <div class="guide-step-num">9</div> 4299 <div class="guide-step-text">(Optional) Add a <strong>title</strong> or a short note if you’d like. You can also post the voice note alone.</div> 4300 </div> 4301
4302 <div class="guide-step"> 4303 <div class="guide-step-num">10</div> 4304 <div class="guide-step-text">Tap <strong>Share</strong>. Your voice note will appear in the Community feed with a little 🎤 label, ready for your sisters to listen.</div> 4305 </div> 4306 </div> 4307 4308 <div class="guide-note" style="border-left-color:var(--berry);"> 4309 <strong>Tip:</strong> Record somewhere quiet. Take a breath before you start. There’s no right way — just your real voice, shared with love. 4310 </div> 4311 4312 <div class="guide-note"> 4313 <strong>A Few Gentle Notes</strong> 4314 <ul style="padding-left:20px;line-height:1.8;margin-top:8px;margin-bottom:0;"> 4315 <li><strong>5-minute maximum</strong> — so we honor each other’s time and attention.</li> 4316 <li><strong>You can delete your own voice notes</strong> any time by tapping the <strong>×</strong> on your post.</li> 4317 <li><strong>Reactions & comments</strong> work the same as any other post — send hearts, emojis, or written replies.</li> 4318 <li><strong>Privacy:</strong> Voice notes only appear in our private Community feed. They are not shared publicly.</li> 4319 </ul> 4320 </div> 4321 4322 </div> 4323 </div> 4324 4325 <!-- Tutorial 2: Journal --> 4326 <div class="tool-card" onclick="this.classList.toggle('open')"> 4327 <div class="tool-card-header"> 4328 <div class="tool-card-header-left"> 4329 <div class="tool-card-icon">✎</div> 4330 <div> 4331 <div class="tool-card-title">How to Use Your Journal</div> 4332 <div class="tool-card-subtitle">Write, reflect, and print your entries</div> 4333 </div> 4334 </div> 4335 <div class="tool-card-arrow">▶</div> 4336 </div> 4337 <div class="tool-card-content"> 4338 4339 <div class="guide-section"> 4340 <div class="guide-section-title">Writing an Entry</div> 4341 4342 <div class="guide-step"> 4343 <div class="guide-step-num">1</div> 4344 <div class="guide-step-text">Tap <strong>Journal</strong> in the sidebar.</div> 4345 </div> 4346 4347 <div class="guide-step"> 4348 <div class="guide-step-num">2</div> 4349 <div class="guide-step-text">You’ll see a text area that says <strong>“What is alive in you right now...”</strong> — write whatever is moving through you.</div> 4350 </div> 4351 4352 <div class="guide-step"> 4353 <div class="guide-step-num">3</div> 4354 <div class="guide-step-text">Before saving, choose your <strong>visibility</strong>:</div> 4355 </div> 4356 </div> 4357 4358 <div class="guide-note"> 4359 <strong>🔒 Private</strong> — Only you can see this entry. It is yours alone. No one else — not even Sumir — can read it. 4360 </div> 4361 4362 <div class="guide-note" style="border-left-color:var(--berry);"> 4363 <strong>👁 Visible to Sumir</strong> — This entry will also be visible to Sumir in a dedicated “Shared With You” section of her journal view. Use this when you want Sumir to witness, read, or hold something with you. 4364 </div> 4365 4366 <div class="guide-section"> 4367 <div class="guide-step"> 4368 <div class="guide-step-num">4</div> 4369 <div class="guide-step-text">Tap <strong>Save Entry</strong>. Your entry will appear below with the date and a badge showing whether it’s private or shared.</div> 4370 </div> 4371 </div> 4372 4373 <div class="guide-divider"></div> 4374
4375 <div class="guide-section"> 4376 <div class="guide-section-title">Managing Your Entries</div> 4377 4378 <div class="guide-step"> 4379 <div class="guide-step-num">✦</div> 4380 <div class="guide-step-text"><strong>Tap any entry</strong> to expand it and see the full text along with action buttons.</div> 4381 </div> 4382 4383 <div class="guide-step"> 4384 <div class="guide-step-num">✎</div> 4385 <div class="guide-step-text"><strong>Edit</strong> — Update the text of an existing entry.</div> 4386 </div> 4387 4388 <div class="guide-step"> 4389 <div class="guide-step-num">♦︎</div> 4390 <div class="guide-step-text"><strong>Share with Sumir / Make Private</strong> — Toggle the visibility of any entry at any time. Changed your mind? Just tap to switch.</div> 4391 </div> 4392 4393 <div class="guide-step"> 4394 <div class="guide-step-num">×</div> 4395 <div class="guide-step-text"><strong>Delete</strong> — Permanently remove an entry you no longer want.</div> 4396 </div> 4397 </div> 4398 4399 <div class="guide-divider"></div> 4400 4401 <div class="guide-section"> 4402 <div class="guide-section-title">Printing Your Journal</div> 4403 4404 <div class="guide-step"> 4405 <div class="guide-step-num">1</div> 4406 <div class="guide-step-text">Tap the <strong>✎ Print My Journal</strong> button that appears above your entries.</div> 4407 </div> 4408 4409 <div class="guide-step"> 4410 <div class="guide-step-num">2</div> 4411 <div class="guide-step-text">A beautifully formatted version of all your entries will be prepared, and your browser’s print dialog will open.</div> 4412 </div> 4413 4414 <div class="guide-step"> 4415 <div class="guide-step-num">3</div> 4416 <div class="guide-step-text">You can print it or save it as a PDF — a beautiful keepsake of your journey.</div> 4417 </div> 4418 </div> 4419 4420 <div class="guide-note"> 4421 <strong>💜 Remember:</strong> Your journal is a sacred space. Write freely, honestly, and without judgment. Your words are yours. 4422 </div> 4423 4424 </div> 4425 </div> 4426 4427 </div> 4428 </div> 4429 </div> 4430 4431 <!-- ===== MONTHLY HARVEST VIEW ===== --> 4432 <div class="view" id="view-harvest"> 4433 <div class="content-body"> 4434 4435 <!-- Teacher: Create new harvest (hidden for students) --> 4436 <div id="harvest-composer" style="display:none;margin-bottom:30px;"> 4437 <div style="background:white;border-radius:14px;border:1px solid rgba(197,165,90,0.25);padding:24px;"> 4438 <div style="font-family:'Marcellus',serif;font-size:10px;letter-spacing:2.5px;color:var(--gold);text-transform:uppercase;margin-bottom:14px;">Create Monthly Harvest</div> 4439 <select id="harvest-month-select" style="width:100%;padding:10px 14px;border:1px solid rgba(197,165,90,0.3);border-radius:8px;font-family:'Lora',serif;font-size:13px;margin-bottom:10px;background:var(--cream);color:var(--charcoal);"> 4440 <option value="">Select Month...</option> 4441 <option value="June 2026">June 2026</option> 4442 <option value="July 2026">July 2026</option> 4443 <option value="August 2026">August 2026</option> 4444 <option value="September 2026">September 2026</option> 4445 <option value="October 2026">October 2026</option> 4446 <option value="November 2026">November 2026</option> 4447 <option value="December 2026">December 2026</option> 4448 <option value="January 2027">January 2027</option> 4449 </select> 4450 <input type="text" id="harvest-title-input" placeholder="Harvest title (optional)" style="width:100%;padding:10px 14px;border:1px solid rgba(197,165,90,0.3);border-radius:8px;font-family:'Lora',serif;font-size:13px;margin-bottom:10px;background:var(--cream);"> 4451 <textarea id="harvest-prompt-input" placeholder="Write the inquiry or prompt for this month..." style="width:100%;padding:14px;border:1px solid rgba(197,165,90,0.3);border-radius:8px;font-family:'Lora',serif;font-size:13px;min-height:120px;resize:vertical;background:var(--cream);"></textarea> 4452 <div style="display:flex;justify-content:flex-end;margin-top:12px;"> 4453 <button onclick="createHarvest()" style="background:var(--berry);color:white;border:none;padding:10px 24px;border-radius:8px;font-family:'Marcellus',serif;font-size:10px;letter-spacing:2px;text-transform:uppercase;cursor:pointer;">Publish Harvest</button> 4454 </div> 4455 </div> 4456 </div> 4457 4458 <!-- Harvest months container --> 4459 <div id="harvest-months-container"> 4460 <div style="text-align:center;padding:40px 20px;color:var(--charcoal);opacity:0.5;">Loading harvests...</div> 4461 </div> 4462 4463 </div> 4464 </div> 4465 4466 <!-- ===== MEMBERS VIEW ===== --> 4467 <div class="view" id="view-members"> 4468 <div class="content-body"> 4469 <div style="margin-bottom:30px;"> 4470 <div style="font-family:'Cormorant Garamond',serif;font-size:22px;color:var(--plum-deep);margin-bottom:4px;">Your Community</div> 4471 <div style="font-size:12px;color:var(--charcoal);opacity:0.6;">Members who have joined the Private Mentorship</div> 4472 </div> 4473 4474 <div id="members-stats" style="display:grid;grid-template-columns:repeat(3,1fr);gap:14px;margin-bottom:30px;"> 4475 <div style="background:white;border-radius:12px;border:1px solid rgba(197,165,90,0.2);padding:20px;text-align:center;"> 4476 <div style="font-family:'Cormorant Garamond',serif;font-size:28px;color:var(--plum-deep);" id="stat-total">—</div> 4477 <div style="font-family:'Marcellus',serif;font-size:9px;letter-spacing:2px;color:var(--gold);text-transform:uppercase;">Total Members</div> 4478 </div> 4479 <div style="background:white;border-radius:12px;border:1px solid rgba(197,165,90,0.2);padding:20px;text-align:center;"> 4480 <div style="font-family:'Cormorant Garamond',serif;font-size:28px;color:var(--plum-deep);" id="stat-active-7">—</div> 4481 <div style="font-family:'Marcellus',serif;font-size:9px;letter-spacing:2px;color:var(--gold);text-transform:uppercase;">Active (7 days)</div> 4482 </div> 4483 <div style="background:white;border-radius:12px;border:1px solid rgba(197,165,90,0.2);padding:20px;text-align:center;"> 4484 <div style="font-family:'Cormorant Garamond',serif;font-size:28px;color:var(--plum-deep);" id="stat-active-30">—</div> 4485 <div style="font-family:'Marcellus',serif;font-size:9px;letter-spacing:2px;color:var(--gold);text-transform:uppercase;">Active (30 days)</div> 4486 </div> 4487 </div> 4488 4489 <div style="background:white;border-radius:14px;border:1px solid rgba(197,165,90,0.2);overflow:hidden;"> 4490 <table style="width:100%;border-collapse:collapse;font-size:13px;"> 4491 <thead> 4492 <tr style="background:var(--cream-warm);border-bottom:1px solid rgba(197,165,90,0.2);"> 4493 <th style="text-align:left;padding:14px 18px;font-family:'Marcellus',serif;font-size:10px;letter-spacing:2px;color:var(--gold);text-transform:uppercase;">Name</th> 4494 <th style="text-align:left;padding:14px 18px;font-family:'Marcellus',serif;font-size:10px;letter-spacing:2px;color:var(--gold);text-transform:uppercase;">Joined</th> 4495 <th style="text-align:left;padding:14px 18px;font-family:'Marcellus',serif;font-size:10px;letter-spacing:2px;color:var(--gold);text-transform:uppercase;">Last Active</th> 4496 <th style="text-align:left;padding:14px 18px;font-family:'Marcellus',serif;font-size:10px;letter-spacing:2px;color:var(--gold);text-transform:uppercase;">Role</th> 4497 </tr> 4498 </thead> 4499 <tbody id="members-table-body"> 4500 <tr><td colspan="4" style="padding:30px;text-align:center;color:var(--charcoal);opacity:0.5;">Loading members...</td></tr> 4501 </tbody> 4502 </table> 4503 </div> 4504 4505 <!-- Library Members Section --> 4506 <div style="margin-top:40px;"> 4507 <div style="font-family:'Cormorant Garamond',serif;font-size:22px;color:var(--plum-deep);margin-bottom:4px;">Library Members</div> 4508 <div style="font-size:12px;color:var(--charcoal);opacity:0.6;margin-bottom:20px;">Everyone who has signed in to the Golden Path Library</div> 4509 4510 <div id="library-members-stats" style="display:grid;grid-template-columns:repeat(2,1fr);gap:14px;margin-bottom:20px;"> 4511 <div style="background:white;border-radius:12px;border:1px solid rgba(197,165,90,0.2);padding:20px;text-align:center;"> 4512 <div style="font-family:'Cormorant Garamond',serif;font-size:28px;color:var(--plum-deep);" id="stat-lib-members">—</div> 4513 <div style="font-family:'Marcellus',serif;font-size:9px;letter-spacing:2px;color:var(--gold);text-transform:uppercase;">Unique Members</div> 4514 </div> 4515 <div style="background:white;border-radius:12px;border:1px solid rgba(197,165,90,0.2);padding:20px;text-align:center;"> 4516 <div style="font-family:'Cormorant Garamond',serif;font-size:28px;color:var(--plum-deep);" id="stat-lib-signins">—</div> 4517 <div style="font-family:'Marcellus',serif;font-size:9px;letter-spacing:2px;color:var(--gold);text-transform:uppercase;">Total Sign-Ins</div> 4518 </div> 4519 </div> 4520 4521 <div style="background:white;border-radius:14px;border:1px solid rgba(197,165,90,0.2);overflow:hidden;margin-bottom:30px;"> 4522 <table style="width:100%;border-collapse:collapse;font-size:13px;"> 4523 <thead> 4524 <tr style="background:var(--cream-warm);border-bottom:1px solid rgba(197,165,90,0.2);"> 4525 <th style="text-align:left;padding:14px 18px;font-family:'Marcellus',serif;font-size:10px;letter-spacing:2px;color:var(--gold);text-transform:uppercase;">Name / Email</th> 4526 <th style="text-align:center;padding:14px 18px;font-family:'Marcellus',serif;font-size:10px;letter-spacing:2px;color:var(--gold);text-transform:uppercase;">Sign-Ins</th> 4527 <th style="text-align:center;padding:14px 18px;font-family:'Marcellus',serif;font-size:10px;letter-spacing:2px;color:var(--gold);text-transform:uppercase;">Favorites</th> 4528 <th style="text-align:left;padding:14px 18px;font-family:'Marcellus',serif;font-size:10px;letter-spacing:2px;color:var(--gold);text-transform:uppercase;">Last Visit</th> 4529 </tr> 4530 </thead> 4531 <tbody id="library-members-body"> 4532 <tr><td colspan="4" style="padding:30px;text-align:center;color:var(--charcoal);opacity:0.5;">Loading library members...</td></tr> 4533 </tbody> 4534 </table> 4535 </div> 4536 </div> 4537 4538 <!-- Library Analytics Section --> 4539 <div style="margin-top:40px;"> 4540 <div style="font-family:'Cormorant Garamond',serif;font-size:22px;color:var(--plum-deep);margin-bottom:4px;">Library Analytics</div> 4541 <div style="font-size:12px;color:var(--charcoal);opacity:0.6;margin-bottom:20px;">Teaching page views & PDF downloads in the Golden Path Library</div> 4542 4543 <div id="library-analytics-stats" style="display:grid;grid-template-columns:repeat(2,1fr);gap:14px;margin-bottom:20px;"> 4544 <div style="background:white;border-radius:12px;border:1px solid rgba(197,165,90,0.2);padding:20px;text-align:center;"> 4545 <div style="font-family:'Cormorant Garamond',serif;font-size:28px;color:var(--plum-deep);" id="stat-lib-views">—</div> 4546 <div style="font-family:'Marcellus',serif;font-size:9px;letter-spacing:2px;color:var(--gold);text-transform:uppercase;">Total Page Views</div> 4547 </div> 4548 <div style="background:white;border-radius:12px;border:1px solid rgba(197,165,90,0.2);padding:20px;text-align:center;"> 4549 <div style="font-family:'Cormorant Garamond',serif;font-size:28px;color:var(--plum-deep);" id="stat-lib-prints">—</div> 4550 <div style="font-family:'Marcellus',serif;font-size:9px;letter-spacing:2px;color:var(--gold);text-transform:uppercase;">PDF Downloads</div> 4551 </div> 4552 </div> 4553 4554 <div style="background:white;border-radius:14px;border:1px solid rgba(197,165,90,0.2);overflow:hidden;"> 4555 <table style="width:100%;border-collapse:collapse;font-size:13px;"> 4556 <thead> 4557 <tr style="background:var(--cream-warm);border-bottom:1px solid rgba(197,165,90,0.2);"> 4558 <th style="text-align:left;padding:14px 18px;font-family:'Marcellus',serif;font-size:10px;letter-spacing:2px;color:var(--gold);text-transform:uppercase;">Teaching</th> 4559 <th style="text-align:center;padding:14px 18px;font-family:'Marcellus',serif;font-size:10px;letter-spacing:2px;color:var(--gold);text-transform:uppercase;">Views</th> 4560 <th style="text-align:center;padding:14px 18px;font-family:'Marcellus',serif;font-size:10px;letter-spacing:2px;color:var(--gold);text-transform:uppercase;">PDF Downloads</th> 4561 </tr> 4562 </thead> 4563 <tbody id="library-analytics-body"> 4564 <tr><td colspan="3" style="padding:30px;text-align:center;color:var(--charcoal);opacity:0.5;">Loading analytics...</td></tr> 4565 </tbody> 4566 </table> 4567 </div> 4568 </div> 4569 4570 </div> 4571 </div> 4572 4573 </main> 4574 4575 <!-- Hidden print area --> 4576 <div id="journal-print-area" style="display:none;"> 4577 <div class="journal-print-header"> 4578 <img src="feminine-rising-logo.png" class="journal-print-logo" alt="Feminine Rising"> 4579 <div class="journal-print-brand">
4579Feminine Rising</div> 4580 <div class="journal-print-title">My Wisdom Journal</div> 4581 <div class="journal-print-name" id="journal-print-name"></div> 4582 </div> 4583 <div id="journal-print-entries"></div> 4584 <div class="journal-print-footer"> 4585 <div class="journal-print-diamond">✦</div> 4586 <div class="journal-print-footer-text">Your words are sacred. Your wisdom is your own.</div> 4587 </div> 4588 </div> 4589 </div> 4590</div> 4591 4592<!-- ============================================ 4593 JAVASCRIPT 4594 ============================================ -->
4595<script> 4596// ============================================================ 4597// SUPABASE CONFIG 4598// Replace these with your Supabase project credentials 4599// ============================================================ 4600const SUPABASE_URL = 'https://ttvjonqfljiinmkjbqvj.supabase.co'; 4601const SUPABASE_ANON_KEY = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6InR0dmpvbnFmbGppaW5ta2picXZqIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NzYyNTkyNDQsImV4cCI6MjA5MTgzNTI0NH0.mQ9NUsw4cFMf2uRxPWoLl2ig04O0lHbqjx7N3hfG3iU'; 4602 4603// Store the createClient factory before our variable shadows the global 4604const _supabaseFactory = window.supabase ? window.supabase.createClient : null; 4605let sbClient = null; 4606let currentUser = null; 4607let currentProfile = null; 4608 4609// Initialize Supabase (only if configured) 4610function initSupabase() { 4611 if (SUPABASE_URL === 'YOUR_SUPABASE_URL') return false; 4612 try { 4613 if (!_supabaseFactory) { 4614 console.error('Supabase library not loaded'); 4615 return false; 4616 } 4617 sbClient = _supabaseFactory(SUPABASE_URL, SUPABASE_ANON_KEY); 4618 return true; 4619 } catch (e) { 4620 console.error('Supabase init error:', e); 4621 return false; 4622 } 4623} 4624 4625// ============================================================ 4626// PASSWORD GATE 4627// ============================================================ 4628const ACCESS_CODE = 'embodied2026'; 4629 4630function checkPassword() { 4631 const input = document.getElementById('password-input').value; 4632 const error = document.getElementById('password-error'); 4633 4634 if (input.toLowerCase() === ACCESS_CODE.toLowerCase()) { 4635 localStorage.setItem('mentorship_authenticated', 'true'); 4636 proceedAfterPassword(); 4637 } else { 4638 error.classList.add('show'); 4639 document.getElementById('password-input').value = ''; 4640 document.getElementById('password-input').focus(); 4641 } 4642} 4643 4644document.getElementById('password-input').addEventListener('keypress', function(e) { 4645 if (e.key === 'Enter') checkPassword(); 4646}); 4647 4648function proceedAfterPassword() { 4649 document.getElementById('password-screen').style.display = 'none'; 4650 4651 if (!initSupabase()) { 4652 // Supabase not configured â show hub directly in demo mode 4653 showHub('Demo User', 'teacher'); 4654 return; 4655 } 4656 4657 // Check for existing session 4658 // Detect if this page load is a password recovery flow. 4659 // Supabase sends users back with #type=recovery (or ?type=recovery) in the URL. 4660 const hash = window.location.hash || ''; 4661 const search = window.location.search || ''; 4662 const isRecovery = hash.includes('type=recovery') || search.includes('type=recovery'); 4663 4664 sbClient.auth.getSession().then(({ data: { session } }) => { 4665 if (isRecovery) { 4666 // Show the "set new password" screen instead of signing the user in 4667 showResetScreen(); 4668 } else if (session) { 4669 handleSignedIn(session.user); 4670 } else { 4671 showAuthScreen(); 4672 } 4673 }); 4674 4675 // Listen for auth state changes (magic link callback / recovery) 4676 sbClient.auth.onAuthStateChange((event, session) => { 4677 if (event === 'PASSWORD_RECOVERY') { 4678 showResetScreen(); 4679 } else if (event === 'SIGNED_IN' && session) { 4680 // If we're in a recovery flow, don't auto-redirect into the hub 4681 if (document.getElementById('reset-screen').style.display === 'flex') return; 4682 handleSignedIn(session.user); 4683 } 4684 }); 4685} 4686 4687function toggleResetPasswordVisibility() { 4688 const inputs = [document.getElementById('reset-password'), document.getElementById('reset-password-confirm')]; 4689 const icons = [document.getElementById('reset-eye-icon'), document.getElementById('reset-eye-icon-2')]; 4690 const shown = inputs[0].type === 'text'; 4691 inputs.forEach(i => { if (i) i.type = shown ? 'password' : 'text'; }); 4692 // ð = shown/visible, ð = hidden 4693 icons.forEach(ic => { if (ic) ic.innerHTML = shown ? '👁' : '🙈'; }); 4694} 4695 4696function toggleAuthPasswordVisibility() { 4697 const input = document.getElementById('auth-password'); 4698 const icon = document.getElementById('auth-eye-icon'); 4699 if (!input) return; 4700 const shown = input.type === 'text'; 4701 input.type = shown ? 'password' : 'text'; 4702 if (icon) icon.innerHTML = shown ? '👁' : '🙈'; 4703} 4704 4705function showResetScreen() { 4706 // Hide other screens 4707 const auth = document.getElementById('auth-screen'); 4708 if (auth) auth.style.display = 'none'; 4709 const name = document.getElementById('name-screen'); 4710 if (name) name.style.display = 'none'; 4711 const hub = document.getElementById('hub-screen'); 4712 if (hub) hub.style.display = 'none'; 4713 const reset = document.getElementById('reset-screen'); 4714 if (reset) reset.style.display = 'flex'; 4715} 4716 4717async function updatePasswordSubmit() { 4718 const pw = document.getElementById('reset-password').value; 4719 const pw2 = document.getElementById('reset-password-confirm').value; 4720 const message = document.getElementById('reset-message'); 4721 const btn = document.getElementById('reset-submit-btn'); 4722 4723 if (!pw || pw.length < 8) { 4724 message.textContent = 'Password must be at least 8 characters.'; 4725 message.className = 'auth-message show'; 4726 return; 4727 } 4728 if (pw !== pw2) { 4729 message.textContent = 'Passwords do not match. Please re-type.'; 4730 message.className = 'auth-message show'; 4731 return; 4732 } 4733 4734 setButtonLoading(btn, true, 'Updating...'); 4735 4736 try { 4737 const { error } = await sbClient.auth.updateUser({ password: pw }); 4738 if (error) { 4739 message.textContent = error.message; 4740 message.className = 'auth-message show'; 4741 setButtonLoading(btn, false); 4742 return; 4743 } 4744 message.textContent = 'Password updated! Taking you to the hub...'; 4745 message.className = 'auth-message show success'; 4746 // Get current user and proceed 4747 const { data: { user } } = await sbClient.auth.getUser(); 4748 setTimeout(() => { 4749 document.getElementById('reset-screen').style.display = 'none'; 4750 // Clear the recovery URL fragment 4751 if (window.history.replaceState) { 4752 window.history.replaceState(null, '', window.location.pathname); 4753 } 4754 if (user) { 4755 welcomeRitualPending = true; 4756 primeAudioContext(); 4757 handleSignedIn(user); 4758 } 4759 }, 900); 4760 } catch (e) { 4761 message.textContent = 'Could not update password. Please try again.'; 4762 message.className = 'auth-message show'; 4763 setButtonLoading(btn, false); 4764 } 4765} 4766 4767// ============================================================ 4768// AUTH (Email + Password) 4769// ============================================================ 4770let authMode = 'signup'; // 'signup' or 'signin' 4771 4772function showAuthScreen() { 4773 document.getElementById('auth-screen').style.display = 'flex'; 4774} 4775 4776function toggleAuthMode() { 4777 const btn = document.getElementById('auth-send-btn'); 4778 const toggle = document.getElementById('auth-toggle'); 4779 const subtitle = document.getElementById('auth-subtitle'); 4780 const passInput = document.getElementById('auth-password'); 4781 4782 if (authMode === 'signup') { 4783 authMode = 'signin'; 4784 btn.textContent = 'Sign In'; 4785 toggle.textContent = 'New here? Create an account'; 4786 subtitle.textContent = 'Sign in to enter the mentorship hub.'; 4787 passInput.placeholder = 'Your password'; 4788 document.getElementById('forgot-row').style.display = 'block'; 4789 } else { 4790 authMode = 'signup'; 4791 btn.textContent = 'Create My Account'; 4792 toggle.textContent = 'Already have an account? Sign in'; 4793 subtitle.textContent = 'Create your account to enter the mentorship hub.'; 4794 passInput.placeholder = 'Create a password'; 4795 document.getElementById('forgot-row').style.display = 'none'; 4796 } 4797 4798 // Clear messages 4799 document.getElementById('auth-message').className = 'auth-message';
4800} 4801 4802async function sendPasswordReset() { 4803 const email = document.getElementById('auth-email').value.trim(); 4804 const message = document.getElementById('auth-message'); 4805 4806 if (!email) { 4807 message.textContent = 'Please enter your email above first, then tap Forgot password.'; 4808 message.className = 'auth-message show'; 4809 return; 4810 } 4811 4812 message.textContent = 'Sending reset link...'; 4813 message.className = 'auth-message show'; 4814 4815 try { 4816 const { error } = await sbClient.auth.resetPasswordForEmail(email, { 4817 redirectTo: window.location.origin + '/mentorship.html' 4818 }); 4819 if (error) { 4820 message.textContent = error.message; 4821 message.className = 'auth-message show'; 4822 } else { 4823 message.textContent = 'Check your email for a password reset link.'; 4824 message.className = 'auth-message show success'; 4825 } 4826 } catch (e) { 4827 message.textContent = 'Could not send reset link. Please try again or email [email protected].'; 4828 message.className = 'auth-message show'; 4829 } 4830} 4831 4832async function authSubmit() { 4833 const email = document.getElementById('auth-email').value.trim(); 4834 const password = document.getElementById('auth-password').value; 4835 const message = document.getElementById('auth-message'); 4836 const btn = document.getElementById('auth-send-btn'); 4837 4838 if (!email) { 4839 message.textContent = 'Please enter your email.'; 4840 message.className = 'auth-message show'; 4841 return; 4842 } 4843 4844 if (!password || password.length < 6) { 4845 message.textContent = 'Password must be at least 6 characters.'; 4846 message.className = 'auth-message show'; 4847 return; 4848 } 4849 4850 btn.disabled = true; 4851 const originalText = btn.textContent; 4852 btn.textContent = 'One moment...'; 4853 4854 let result; 4855 4856 if (authMode === 'signup') { 4857 result = await sbClient.auth.signUp({ email, password }); 4858 if (result.error) { 4859 // If user already exists, try signing in instead 4860 if (result.error.message.includes('already registered') || result.error.message.includes('already been registered')) { 4861 result = await sbClient.auth.signInWithPassword({ email, password }); 4862 } 4863 } 4864 } else { 4865 result = await sbClient.auth.signInWithPassword({ email, password }); 4866 } 4867 4868 if (result.error) { 4869 message.textContent = result.error.message; 4870 message.className = 'auth-message show'; 4871 btn.disabled = false; 4872 btn.textContent = originalText; 4873 } else if (result.data?.user) { 4874 // Fresh sign-in: trigger the welcome ritual when the hub opens. 4875 welcomeRitualPending = true; 4876 // Pre-warm an AudioContext during this synchronous click event so 4877 // browsers will allow the chime to play later (asynchronous code 4878 // loses the user-gesture context otherwise). 4879 primeAudioContext(); 4880 handleSignedIn(result.data.user); 4881 } 4882} 4883 4884// Holds the pre-warmed AudioContext for the welcome chime 4885let _welcomeAudioCtx = null; 4886function primeAudioContext() { 4887 try { 4888 if (_welcomeAudioCtx) return; 4889 _welcomeAudioCtx = new (window.AudioContext || window.webkitAudioContext)(); 4890 if (_welcomeAudioCtx.state === 'suspended') { 4891 _welcomeAudioCtx.resume().catch(() => {}); 4892 } 4893 } catch (e) { 4894 _welcomeAudioCtx = null; 4895 } 4896} 4897 4898document.getElementById('auth-email').addEventListener('keypress', function(e) { 4899 if (e.key === 'Enter') document.getElementById('auth-password').focus(); 4900}); 4901document.getElementById('auth-password').addEventListener('keypress', function(e) { 4902 if (e.key === 'Enter') authSubmit(); 4903}); 4904 4905// ============================================================ 4906// EMOJI PICKER 4907// ============================================================ 4908const emojiData = { 4909 'Favorites': ['â¤ï¸','ð','â¨','ð','ð¹','ð«','ð¦','ð¥','ð','ð¸','ð¿','ð','ðº','âï¸','ðï¸','ð'], 4910 'Feelings': ['ð','ð¥°','ð','ð','ð¤','ð¢','ð','ðª','ð','ð','ð¤','ð','ð«¶','ð','ð¥¹','â'], 4911 'Nature': ['ð»','ð·','ð','â','ð','ð','ð','ðª·','ð¾','ðª»','ð±','ð','ð','ðª´','ð','ðï¸'], 4912 'Symbols': ['â¥ï¸','â¦','â','â','â½','â¡','â¡','ð¯','ð¿','ð¯ï¸','ðª¶','ð¶','ð','ðï¸','ð§¿','ðª¬'] 4913}; 4914 4915let activeEmojiTarget = null; 4916 4917function toggleEmojiPicker(targetId) { 4918 const picker = document.getElementById('emoji-picker-' + targetId); 4919 4920 // Close any other open pickers 4921 document.querySelectorAll('.emoji-picker').forEach(p => { 4922 if (p !== picker) p.style.display = 'none'; 4923 }); 4924 4925 if (picker.style.display === 'none') { 4926 // Build picker content if empty 4927 if (!picker.innerHTML) { 4928 let html = ''; 4929 for (const [category, emojis] of Object.entries(emojiData)) { 4930 html += `<div class="emoji-category-label">${category}</div><div class="emoji-picker-grid">`; 4931 emojis.forEach(e => { 4932 html += `<button class="emoji-btn" onclick="insertEmoji('${e}', '${targetId}')">${e}</button>`; 4933 }); 4934 html += '</div>'; 4935 } 4936 picker.innerHTML = html; 4937 } 4938 picker.style.display = 'block'; 4939 activeEmojiTarget = targetId; 4940 } else { 4941 picker.style.display = 'none'; 4942 activeEmojiTarget = null; 4943 } 4944} 4945 4946function insertEmoji(emoji, targetId) { 4947 const textarea = document.getElementById(targetId); 4948 if (!textarea) return; 4949 4950 const start = textarea.selectionStart; 4951 const end = textarea.selectionEnd; 4952 const text = textarea.value; 4953 textarea.value = text.substring(0, start) + emoji + text.substring(end); 4954 textarea.selectionStart = textarea.selectionEnd = start + emoji.length; 4955 textarea.focus(); 4956} 4957 4958// Close emoji picker when clicking outside 4959document.addEventListener('click', (e) => { 4960 if (!e.target.closest('.emoji-picker') && !e.target.closest('.media-upload-btn') && !e.target.closest('.comment-emoji-btn')) { 4961 document.querySelectorAll('.emoji-picker').forEach(p => p.style.display = 'none'); 4962 } 4963}); 4964 4965function previewHub() { 4966 document.getElementById('auth-screen').style.display = 'none'; 4967 showHub('Sister', 'teacher'); 4968} 4969 4970// ============================================================ 4971// USER SESSION 4972// ============================================================ 4973async function handleSignedIn(user) { 4974 currentUser = user; 4975 document.getElementById('auth-screen').style.display = 'none'; 4976 4977 // Fetch profile 4978 let { data: profile } = await sbClient 4979 .from('profiles') 4980 .select('*') 4981 .eq('id', user.id) 4982 .single(); 4983 4984 // If the trigger didn't fire, create the profile now 4985 if (!profile) { 4986 const { data: created } = await sbClient 4987 .from('profiles') 4988 .upsert({ id: user.id, display_name: 'Sister', role: 'student' }) 4989 .select() 4990 .single(); 4991 profile = created; 4992 } 4993 4994 if (profile) { 4995 currentProfile = profile; 4996 sbClient.from('profiles').update({ last_login: new Date().toISOString() }).eq('id', user.id); 4997 if (profile.display_name === 'Sister') { 4998 showNameScreen(); 4999 } else { 5000 showHub(profile.display_name, profile.role); 5001 } 5002 } else { 5003 currentProfile = null; 5004 showNameScreen(); 5005 } 5006} 5007 5008function showNameScreen() { 5009 document.getElementById('name-screen').style.display = 'flex'; 5010} 5011 5012async function saveName() { 5013 const name = document.getElementById('name-input').value.trim(); 5014 if (!name) return; 5015 5016 const btn = document.querySelector('#name-screen .auth-btn'); 5017 setButtonLoading(btn, true, 'One moment...'); 5018 5019 if (sbClient && currentUser) { 5020 const isNew = !currentProfile || !currentProfile.id; 5021 // Upsert handles both create and update in one call 5022 const { data, error } = await sbClient 5023 .from('profiles') 5024 .upsert({ id: currentUser.id, display_name: name, role: currentProfile?.role || 'student' }) 5025 .select() 5026 .single(); 5027 5028 if (error) { 5029 console.error('Profile save error:', error); 5030 showToast('Could not save your name â please try again.', 'error'); 5031 setButtonLoading(btn, false); 5032 return; 5033 } 5034 5035 currentProfile = data || { id: currentUser.id, display_name: name, role: 'student' }; 5036 5037 if (isNew) {
5038 try { 5039 const formData = new URLSearchParams(); 5040 formData.append('form-name', 'new-member-signup'); 5041 formData.append('member-name', name); 5042 formData.append('member-email', currentUser.email || ''); 5043 formData.append('signup-date', new Date().toLocaleString()); 5044 fetch('/', { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: formData.toString() }).catch(() => {}); 5045 } catch(e) {} 5046 } 5047 } 5048 5049 document.getElementById('name-screen').style.display = 'none'; 5050 showHub(name, currentProfile ? currentProfile.role : 'student'); 5051} 5052 5053document.getElementById('name-input').addEventListener('keypress', function(e) { 5054 if (e.key === 'Enter') saveName(); 5055}); 5056 5057async function signOut() { 5058 if (sbClient) { 5059 await sbClient.auth.signOut(); 5060 } 5061 localStorage.removeItem('mentorship_authenticated'); 5062 currentUser = null; 5063 currentProfile = null; 5064 window.location.reload(); 5065} 5066 5067// ============================================================ 5068// HUB 5069// ============================================================ 5070// ============================================================ 5071// PRESENCE â "Sumir is in the room" 5072// ============================================================ 5073async function togglePresence() { 5074 if (!sbClient || !currentUser || !currentProfile || currentProfile.role !== 'teacher') return; 5075 5076 const newState = !currentProfile.is_present; 5077 const { error } = await sbClient 5078 .from('profiles') 5079 .update({ is_present: newState }) 5080 .eq('id', currentUser.id); 5081 if (error) { 5082 showToast('Could not update presence: ' + error.message, 'error'); 5083 return; 5084 } 5085 currentProfile.is_present = newState; 5086 updatePresenceUI(newState, true); 5087 showToast(newState ? 'You are now visible as "in the room"' : 'You are now away', 'success'); 5088} 5089 5090function updatePresenceUI(teacherIsPresent, viewerIsTeacher) { 5091 // Banner on home view 5092 const banner = document.getElementById('presence-banner'); 5093 if (banner) banner.style.display = teacherIsPresent ? 'inline-flex' : 'none'; 5094 5095 // Sidebar dot â shown next to teacher's name when she's present (visible to everyone if presence is on) 5096 const dot = document.getElementById('sidebar-presence-dot'); 5097 if (dot && viewerIsTeacher) { 5098 dot.style.display = teacherIsPresent ? 'inline-block' : 'none'; 5099 } 5100 5101 // Toggle button label (teacher only) 5102 const toggleBtn = document.getElementById('sidebar-presence-toggle'); 5103 const toggleLabel = document.getElementById('sidebar-presence-label'); 5104 if (toggleBtn && toggleLabel && viewerIsTeacher) { 5105 if (teacherIsPresent) { 5106 toggleBtn.classList.add('is-here'); 5107 toggleLabel.innerHTML = '❤ In the Room'; 5108 } else { 5109 toggleBtn.classList.remove('is-here'); 5110 toggleLabel.innerHTML = '♡ Mark as Here'; 5111 } 5112 } 5113} 5114 5115async function refreshPresence() { 5116 if (!sbClient) return; 5117 try { 5118 // Look up the teacher's current presence 5119 const { data: teacher } = await sbClient 5120 .from('profiles') 5121 .select('is_present') 5122 .eq('role', 'teacher') 5123 .limit(1) 5124 .single(); 5125 const teacherIsPresent = !!(teacher && teacher.is_present); 5126 const viewerIsTeacher = currentProfile && currentProfile.role === 'teacher'; 5127 updatePresenceUI(teacherIsPresent, true /* always show banner regardless of viewer */); 5128 // For dot beside teacher's name, only show if viewer IS the teacher 5129 const dot = document.getElementById('sidebar-presence-dot'); 5130 if (dot) dot.style.display = (viewerIsTeacher && teacherIsPresent) ? 'inline-block' : 'none'; 5131 } catch (e) { 5132 // No teacher row found yet â silently ignore 5133 } 5134} 5135 5136// ============================================================ 5137// WELCOME RITUAL â gold star sparkles + soft singing-bowl chime 5138// Plays once per session right when the hub opens. 5139// ============================================================ 5140function playWelcomeChime(existingCtx) { 5141 try { 5142 const ctx = existingCtx || new (window.AudioContext || window.webkitAudioContext)(); 5143 // If suspended, try to resume (works inside user-gesture chains) 5144 if (ctx.state === 'suspended') { 5145 ctx.resume().catch(() => {}); 5146 } 5147 // If still suspended after attempting resume, give up gracefully 5148 if (ctx.state === 'suspended' && !existingCtx) { 5149 // We don't close it â it may resume after the user clicks the sound fallback 5150 return false;
5151 } 5152 const now = ctx.currentTime; 5153 5154 // Master gain â very gentle 5155 const master = ctx.createGain(); 5156 master.gain.setValueAtTime(0.00001, now); 5157 master.gain.linearRampToValueAtTime(0.025, now + 2.0); 5158 master.gain.exponentialRampToValueAtTime(0.0001, now + 22); 5159 master.connect(ctx.destination); 5160 5161 // Warm low-pass filter 5162 const filter = ctx.createBiquadFilter(); 5163 filter.type = 'lowpass'; 5164 filter.frequency.setValueAtTime(1800, now); 5165 filter.frequency.exponentialRampToValueAtTime(450, now + 22); 5166 filter.Q.value = 0.35; 5167 filter.connect(master); 5168 5169 // Sweet bowl partials (G3 fundamental + octave + fifth + soft inharmonic) 5170 const fundamental = 196; 5171 const partials = [ 5172 { ratio: 1.0, vol: 1.00, settle: 0.997 }, 5173 { ratio: 2.0, vol: 0.32, settle: 0.998 }, 5174 { ratio: 1.5, vol: 0.22, settle: 0.997 }, 5175 { ratio: 2.76, vol: 0.16, settle: 0.996 }, 5176 { ratio: 1.005, vol: 0.35, settle: 0.998 } 5177 ]; 5178 partials.forEach((p, i) => { 5179 const osc = ctx.createOscillator(); 5180 const g = ctx.createGain(); 5181 osc.type = 'sine'; 5182 osc.frequency.value = fundamental * p.ratio; 5183 osc.frequency.linearRampToValueAtTime(fundamental * p.ratio * p.settle, now + 16); 5184 g.gain.setValueAtTime(0.00001, now); 5185 g.gain.linearRampToValueAtTime(p.vol, now + 1.0 + i * 0.15); 5186 const decayTime = 22 - i * 1.2; 5187 g.gain.exponentialRampToValueAtTime(0.00001, now + decayTime); 5188 osc.connect(g); 5189 g.connect(filter); 5190 osc.start(now); 5191 osc.stop(now + decayTime + 0.5); 5192 }); 5193 return true; 5194 } catch (e) { 5195 return false; 5196 } 5197} 5198 5199// Soft, glowing 4-pointed sparkle (built as inline SVG for proper glow + control). 5200// Two crossed elongated diamonds = classic "twinkle" shape used in animation. 5201function makeSparkleSvg(hue, lightness) { 5202 // Returns the SVG string. The gradient gives a luminous center, fading to soft gold. 5203 const id = 'g' + Math.floor(Math.random() * 1000000); 5204 return `<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"> 5205 <defs> 5206 <radialGradient id="${id}" cx="50%" cy="50%" r="50%"> 5207 <stop offset="0%" stop-color="hsl(${hue}, 100%, 92%)" stop-opacity="1" /> 5208 <stop offset="40%" stop-color="hsl(${hue}, 80%, ${lightness}%)" stop-opacity="0.95" /> 5209 <stop offset="100%" stop-color="hsl(${hue}, 70%, ${lightness - 10}%)" stop-opacity="0" /> 5210 </radialGradient> 5211 </defs> 5212 <!-- vertical diamond --> 5213 <path d="M12 0 L13.4 10.6 L12 24 L10.6 10.6 Z" fill="url(#${id})" /> 5214 <!-- horizontal diamond --> 5215 <path d="M0 12 L10.6 10.6 L24 12 L10.6 13.4 Z" fill="url(#${id})" /> 5216 <!-- soft center --> 5217 <circle cx="12" cy="12" r="2.2" fill="hsl(${hue}, 100%, 95%)" opacity="0.85" /> 5218 </svg>`; 5219} 5220 5221function spawnWelcomeStars() { 5222 const logo = document.getElementById('home-logo'); 5223 if (!logo) return; 5224 5225 const rect = logo.getBoundingClientRect(); 5226 const centerX = rect.left + rect.width / 2; 5227 const centerY = rect.top + rect.height / 2; 5228 5229 const COUNT = 95; 5230 for (let i = 0; i < COUNT; i++) { 5231 const star = document.createElement('div'); 5232 // ~40% of stars go BEHIND the tiles for depth/parallax effect 5233 const isBehind = Math.random() < 0.40; 5234 star.className = isBehind ? 'welcome-star behind' : 'welcome-star'; 5235 5236 // Varied sizes â behind stars tend to be smaller (depth perception) 5237 const size = isBehind 5238 ? 6 + Math.random() * 12 // behind: 6â18px (smaller, distant) 5239 : 10 + Math.random() * 16; // front: 10â26px (larger, closer) 5240 star.style.width = size + 'px'; 5241 star.style.height = size + 'px'; 5242 5243 // Color variation in the gold range (HSL) 5244 const hue = 36 + Math.random() * 10; // gold-yellow range 5245 const lightness = 55 + Math.random() * 20; // 55â75 5246 star.innerHTML = makeSparkleSvg(hue, lightness); 5247 5248 // Stagger the twinkle animation so they don't pulse in sync
5249 const twinkleDelay = -Math.random() * 1.8; // negative delay = phase offset 5250 const twinkleSvg = star.querySelector('svg'); 5251 if (twinkleSvg) twinkleSvg.style.animationDelay = twinkleDelay + 's'; 5252 5253 // Position at the logo's center 5254 star.style.left = (centerX - size / 2) + 'px'; 5255 star.style.top = (centerY - size / 2) + 'px'; 5256 star.style.opacity = '0'; 5257 5258 document.body.appendChild(star); 5259 5260 // Drift outward â bias upward like fairy dust 5261 const angle = (Math.random() - 0.5) * Math.PI * 1.7 - Math.PI / 2; 5262 const dist = 200 + Math.random() * 400; 5263 const dx = Math.cos(angle) * dist; 5264 const dy = Math.sin(angle) * dist; 5265 5266 // Longer overall drift + much more spread-out staggering for ethereal release 5267 const duration = 16000 + Math.random() * 9000; // 16â25 seconds 5268 const delay = 200 + Math.random() * 7500; // 0.2â7.7s â releases over a longer window 5269 5270 // Random rotation for sparkle variety 5271 const rot = (Math.random() - 0.5) * 360; 5272 5273 star.animate( 5274 [ 5275 { transform: `translate(0, 0) scale(0.3) rotate(0deg)`, opacity: 0 }, 5276 { transform: `translate(${dx * 0.10}px, ${dy * 0.10}px) scale(0.85) rotate(${rot * 0.15}deg)`, opacity: 0.85, offset: 0.10 }, 5277 { transform: `translate(${dx * 0.25}px, ${dy * 0.25}px) scale(1) rotate(${rot * 0.3}deg)`, opacity: 0.95, offset: 0.22 }, 5278 { transform: `translate(${dx * 0.55}px, ${dy * 0.55}px) scale(1.1) rotate(${rot * 0.55}deg)`, opacity: 0.85, offset: 0.55 }, 5279 { transform: `translate(${dx * 0.78}px, ${dy * 0.78}px) scale(0.95) rotate(${rot * 0.78}deg)`, opacity: 0.55, offset: 0.78 }, 5280 { transform: `translate(${dx}px, ${dy}px) scale(0.6) rotate(${rot}deg)`, opacity: 0 } 5281 ], 5282 { 5283 duration, 5284 delay, 5285 easing: 'cubic-bezier(0.18, 0.55, 0.40, 1)', // gentler easing 5286 fill: 'forwards' 5287 } 5288 ); 5289 5290 setTimeout(() => star.remove(), duration + delay + 200); 5291 } 5292} 5293 5294// Module-level flag: set to true when a fresh sign-in happens. 5295// This way "page refresh with existing session" doesn't trigger the ritual, 5296// but every time someone enters their email/password, it does. 5297let welcomeRitualPending = false; 5298 5299function playWelcomeRitual() { 5300 if (!welcomeRitualPending) return; 5301 welcomeRitualPending = false; 5302 5303 // Quick start â sparkles begin almost immediately 5304 setTimeout(() => spawnWelcomeStars(), 80); 5305 5306 // Try to play the chime using the pre-warmed AudioContext (created during the 5307 // user's "Sign In" click). Only show the fallback if the browser STILL blocks audio. 5308 setTimeout(() => { 5309 const ok = playWelcomeChime(_welcomeAudioCtx); 5310 if (!ok) showSoundFallback(); 5311 }, 80); 5312 5313 // Show onboarding popup (first login only) 5314 showOnboardingPopup(); 5315} 5316 5317// ============================================================ 5318// ONBOARDING POPUP â gentle nudge toward the sidebar 5319// Shows once per user (tracked in localStorage), auto-dismisses 5320// after 10 seconds or when user clicks a sidebar link. 5321// ============================================================ 5322function showOnboardingPopup() { 5323 const storageKey = 'fr_onboarding_seen'; 5324 if (localStorage.getItem(storageKey)) return; 5325 5326 // Create overlay container 5327 const overlay = document.createElement('div'); 5328 overlay.className = 'onboarding-overlay'; 5329 overlay.innerHTML = ` 5330 <div class="onboarding-popup"> 5331 <div class="onboarding-popup-title">Explore Your Private Mentorship Portal</div> 5332 <div class="onboarding-popup-text">Take a look at all of the options on the left-hand side — each one has been lovingly prepared for you.</div> 5333 </div> 5334 `; 5335 5336 // Create animated arrow 5337 const arrow = document.createElement('div'); 5338 arrow.className = 'onboarding-arrow'; 5339 arrow.innerHTML = `<svg viewBox="0 0 24 24"><path d="M19 12H5M5 12l6-6M5 12l6 6"/></svg>`; 5340 5341 document.body.appendChild(overlay); 5342 document.body.appendChild(arrow); 5343 5344 // Fade in the arrow in sync with the overlay 5345 arrow.style.opacity = '0';
5346 arrow.style.animation = 'none'; 5347 setTimeout(() => { 5348 arrow.style.transition = 'opacity 0.8s ease'; 5349 arrow.style.opacity = '1'; 5350 arrow.style.animation = 'arrowBounce 1.2s ease-in-out infinite'; 5351 }, 6000); 5352 5353 function dismiss() { 5354 localStorage.setItem(storageKey, 'true'); 5355 // Kill the animation so the transition can take over 5356 overlay.style.animation = 'none'; 5357 overlay.style.opacity = '1'; 5358 // Force reflow, then fade out 5359 void overlay.offsetWidth; 5360 overlay.style.transition = 'opacity 1.2s ease'; 5361 overlay.style.opacity = '0'; 5362 arrow.style.transition = 'opacity 1.2s ease'; 5363 arrow.style.opacity = '0'; 5364 setTimeout(() => { 5365 overlay.remove(); 5366 arrow.remove(); 5367 }, 1400); 5368 // Clean up listener 5369 document.querySelectorAll('.sidebar-link').forEach(link => { 5370 link.removeEventListener('click', dismiss); 5371 }); 5372 clearTimeout(autoTimer); 5373 } 5374 5375 // Auto-dismiss: visible by 7.5s, stays 7 more seconds = 14.5s total 5376 const autoTimer = setTimeout(dismiss, 14500); 5377 5378 // Dismiss on sidebar click 5379 document.querySelectorAll('.sidebar-link').forEach(link => { 5380 link.addEventListener('click', dismiss); 5381 }); 5382} 5383 5384// Centered, more visible "tap for sound" pill shown only if browser blocked the chime 5385function showSoundFallback() { 5386 if (document.getElementById('welcome-sound-toggle')) return; 5387 const btn = document.createElement('button'); 5388 btn.id = 'welcome-sound-toggle'; 5389 btn.setAttribute('aria-label', 'Tap for soft chime'); 5390 btn.innerHTML = '<span style="font-size:18px;">🔊</span> Tap for soft chime'; 5391 btn.style.cssText = 'position:fixed;top:24px;left:50%;transform:translateX(-50%);padding:11px 22px;border-radius:24px;background:rgba(255,255,255,0.95);border:1px solid var(--gold);color:var(--berry);font-family:Marcellus,serif;font-size:11px;letter-spacing:1.5px;text-transform:uppercase;cursor:pointer;z-index:200;display:flex;align-items:center;justify-content:center;transition:all 0.3s;box-shadow:0 4px 20px rgba(148,14,75,0.2);animation:soundPillPulse 2s ease infinite;'; 5392 btn.onclick = function() { 5393 playWelcomeChime(); 5394 btn.style.transition = 'all 0.4s ease'; 5395 btn.style.opacity = '0'; 5396 btn.style.transform = 'translateX(-50%) translateY(-10px)'; 5397 setTimeout(() => btn.remove(), 400); 5398 }; 5399 document.body.appendChild(btn); 5400 // Inject a pulse keyframe if not present 5401 if (!document.getElementById('sound-pill-keyframes')) { 5402 const style = document.createElement('style'); 5403 style.id = 'sound-pill-keyframes'; 5404 style.textContent = '@keyframes soundPillPulse { 0%,100% { box-shadow: 0 4px 20px rgba(148,14,75,0.2); } 50% { box-shadow: 0 4px 28px rgba(148,14,75,0.4); } }'; 5405 document.head.appendChild(style); 5406 } 5407 // Auto-fade after 18 seconds (longer than before so she has time to notice) 5408 setTimeout(() => { 5409 if (!btn.parentNode) return; 5410 btn.style.opacity = '0'; 5411 setTimeout(() => btn.remove(), 1200); 5412 }, 18000); 5413} 5414 5415function showHub(displayName, role) { 5416 document.getElementById('password-screen').style.display = 'none'; 5417 document.getElementById('auth-screen').style.display = 'none'; 5418 document.getElementById('name-screen').style.display = 'none'; 5419 document.getElementById('hub').style.display = 'block'; 5420 5421 document.getElementById('sidebar-user-name-text').textContent = displayName; 5422 5423 // Set greeting: "Welcome" on first visit, "Welcome back" on return visits 5424 // Use a flag to avoid double-setting when onAuthStateChange re-triggers showHub 5425 const greetingKey = 'fr_has_visited'; 5426 const greetingEl = document.getElementById('home-greeting'); 5427 if (greetingEl && !window._greetingSet) { 5428 window._greetingSet = true; 5429 if (localStorage.getItem(greetingKey)) { 5430 greetingEl.textContent = 'Welcome back'; 5431 } else { 5432 greetingEl.textContent = 'Welcome'; 5433 } 5434 // Mark as visited AFTER setting the greeting, with a delay to avoid race conditions 5435 setTimeout(() => localStorage.setItem(greetingKey, 'true'), 3000); 5436 } 5437 5438 // Set composer avatar 5439 const avatarEl = document.getElementById('composer-avatar'); 5440 if (avatarEl) avatarEl.textContent = displayName.charAt(0).toUpperCase(); 5441 5442 // Show composer for all authenticated users 5443 document.getElementById('post-composer').style.display = 'block'; 5444 5445 // Hide teacher-only post types (Teaching, Announcement) for students, default to Reflection 5446 if (role !== 'teacher') { 5447 document.querySelectorAll('.teacher-only-option').forEach(opt => opt.style.display = 'none'); 5448 document.getElementById('compose-type').value = 'reflection'; 5449 } 5450 5451 // Show members link, CRM link, and harvest composer for teachers 5452 const membersLink = document.getElementById('sidebar-members-link'); 5453 if (membersLink && role === 'teacher') membersLink.style.display = ''; 5454 const crmLink = document.getElementById('sidebar-crm-link'); 5455 if (crmLink && role === 'teacher') crmLink.style.display = ''; 5456 5457 // Show presence toggle (teacher only) and check current presence state for everyone 5458 if (role === 'teacher') { 5459 const presenceToggle = document.getElementById('sidebar-presence-toggle'); 5460 if (presenceToggle) presenceToggle.style.display = 'block'; 5461 } 5462 refreshPresence(); 5463 // Refresh presence every 60 seconds for everyone 5464 if (window._presenceTimer) clearInterval(window._presenceTimer); 5465 window._presenceTimer = setInterval(refreshPresence, 60000); 5466 5467 const harvestComposer = document.getElementById('harvest-composer'); 5468 if (harvestComposer && role === 'teacher') harvestComposer.style.display = 'block'; 5469 5470 // Check for hash-based routing 5471 const hash = window.location.hash.replace('#', ''); 5472 if (hash && ['home', 'feed', 'about', 'teachings', 'replays', 'journal', 'harvest', 'sc
5472hedule', 'tools', 'members'].includes(hash)) { 5473 switchView(hash); 5474 } else { 5475 switchView('home'); 5476 } 5477 5478 // Welcome ritual: gold star sparkles + soft chime when entering the hub 5479 // (only fires once per session, only on the home view) 5480 if (!hash || hash === 'home') { 5481 playWelcomeRitual(); 5482 } 5483 5484 // Load feed 5485 loadFeed(); 5486 setupRealtime(); 5487} 5488 5489// ============================================================ 5490// VIEW SWITCHING 5491// ============================================================ 5492const VIEW_TITLES = { 5493 feed: ['Private Mentorship', 'Community'], 5494 about: ['Private Mentorship', 'About'], 5495 teachings: ['Private Mentorship', 'Teachings & Transmissions'], 5496 replays: ['Private Mentorship', 'Replays'], 5497 home: ['Private Mentorship', ''], 5498 journal: ['Private Mentorship', 'Your Wisdom Journal'], 5499 schedule: ['Private Mentorship', 'Schedule'], 5500 tools: ['Private Mentorship', 'Tools & Guides'], 5501 harvest: ['Private Mentorship', 'Monthly Harvest'], 5502 members: ['Private Mentorship', 'Members & Analytics'] 5503}; 5504 5505function switchView(viewName) { 5506 // Update sidebar 5507 document.querySelectorAll('.sidebar-link').forEach(link => { 5508 link.classList.toggle('active', link.dataset.view === viewName); 5509 }); 5510 5511 // Update views 5512 document.querySelectorAll('.view').forEach(view => { 5513 view.classList.remove('active'); 5514 }); 5515 document.getElementById('view-' + viewName).classList.add('active'); 5516 5517 // Update header 5518 const [eyebrow, title] = VIEW_TITLES[viewName] || ['Private Mentorship', viewName]; 5519 document.getElementById('header-eyebrow').textContent = eyebrow; 5520 document.getElementById('header-title').textContent = title; 5521 5522 // Hide header on home view 5523 const headerEl = document.querySelector('.content-header'); 5524 if (headerEl) headerEl.style.display = viewName === 'home' ? 'none' : 'block'; 5525 5526 // Update home greeting name 5527 if (viewName === 'home') { 5528 const name = document.getElementById('sidebar-user-name-text').textContent || 'Sister'; 5529 document.getElementById('home-user-name').textContent = name; 5530 } 5531 5532 // Update URL hash 5533 window.location.hash = viewName; 5534 5535 // Scroll to top 5536 window.scrollTo(0, 0); 5537 5538 // Close mobile sidebar 5539 document.querySelector('.sidebar').classList.remove('open'); 5540 document.querySelector('.sidebar-overlay').classList.remove('show'); 5541 5542 // Load view-specific data 5543 if (viewName === 'teachings') loadTeachings(); 5544 if (viewName === 'journal') { loadJournal(); loadInquiryResponses(); } 5545 if (viewName === 'replays') loadReplays(); 5546 if (viewName === 'harvest') loadHarvests(); 5547 if (viewName === 'members') loadMembers(); 5548} 5549 5550function toggleSidebar() { 5551 document.querySelector('.sidebar').classList.toggle('open'); 5552 document.querySelector('.sidebar-overlay').classList.toggle('show'); 5553} 5554 5555// ============================================================ 5556// COMMUNITY FEED 5557// ============================================================ 5558async function loadFeed() { 5559 const container = document.getElementById('feed-container'); 5560 5561 if (!sbClient) { 5562 container.innerHTML = renderDemoFeed(); 5563 return; 5564 } 5565 5566 const { data: posts, error } = await sbClient 5567 .from('posts') 5568 .select(` 5569 *, 5570 author:profiles!posts_author_id_fkey(display_name, role), 5571 reflections( 5572 *, 5573 author:profiles!reflections_author_id_fkey(display_name, role), 5574 hearts(user_id), 5575 reflection_reactions(id, user_id, emoji) 5576 ), 5577 hearts(user_id), 5578 post_reactions(id, user_id, emoji) 5579 `) 5580 .order('created_at', { ascending: false }); 5581 5582 if (error) { 5583 container.innerHTML = '<p style="text-align:center;color:var(--berry);">Unable to load feed.</p>'; 5584 return; 5585 } 5586 5587 if (!posts || posts.length === 0) { 5588 container.innerHTML = ` 5589 <div class="feed-empty"> 5590 <div class="feed-empty-icon">✦</div> 5591 <p class="feed-empty-text">The circle is quiet for now.</p> 5592 </div>`; 5593 return; 5594 } 5595 5596 container.innerHTML = posts.map(post => renderPost(post)).join(''); 5597 5598 // Show Hidden filter button for teachers 5599 const hiddenBtn = document.getElementById('feed-hidden-btn'); 5600 if (hiddenBtn && currentProfile && currentProfile.role === 'teacher') { 5601 const hiddenCount = posts.filter(p => p.hidden_from_feed).length; 5602 if (hiddenCount > 0) { 5603 hiddenBtn.style.display = ''; 5604 hiddenBtn.textContent = 'Hidden (' + hiddenCount + ')'; 5605 } else { 5606 hiddenBtn.style.display = 'none'; 5607 } 5608 } 5609 5610 // Apply filters to hide hidden posts by default 5611 applyFeedFilters(); 5612 5613 // Preload voice note audio as blobs so play works instantly 5614 preloadVoiceNotes(); 5615 5616 // Teacher-only: offer to convert any legacy WebM notes to MP3 5617 checkLegacyVoiceNotes(); 5618} 5619 5620function renderPost(post) { 5621 const authorName = post.author?.display_name || 'Sister'; 5622 const isTeacher = post.author?.role === 'teacher';
5623 const date = new Date(post.created_at).toLocaleDateString('en-US', { month: 'long', day: 'numeric', year: 'numeric' }); 5624 const heartCount = post.hearts ? post.hearts.length : 0; 5625 const isHearted = post.hearts && currentUser ? post.hearts.some(h => h.user_id === currentUser.id) : false; 5626 const isOwner = currentUser && post.author_id === currentUser.id; 5627 const isCurrentTeacher = currentProfile && currentProfile.role === 'teacher'; 5628 const typeLabel = post.post_type === 'prompt' ? 'Journal Prompt' 5629 : post.post_type === 'announcement' ? '⦠Announcement' 5630 : post.post_type === 'reflection' ? 'Reflection' 5631 : post.post_type === 'voice_note' ? 'ð¤ Voice Note' 5632 : 'Teaching'; 5633 5634 const reflections = post.reflections || []; 5635 reflections.sort((a, b) => new Date(a.created_at) - new Date(b.created_at)); 5636 5637 // Media HTML â voice notes get a special inline player 5638 let mediaHtml = ''; 5639 if (post.media_url) { 5640 if (post.media_type === 'audio' || post.post_type === 'voice_note') { 5641 const vnId = 'vn-' + post.id; 5642 mediaHtml = `<div class="voice-note-player" id="${vnId}" data-url="${post.media_url}"> 5643 <button class="vn-play-btn" onclick="vnToggle('${vnId}')" aria-label="Play voice note">▶︎</button> 5644 <div class="vn-controls"> 5645 <div class="voice-note-label">Voice Note from ${escapeHtml(authorName)}</div> 5646 <div class="vn-progress-wrap" onclick="vnSeek(event,'${vnId}')"> 5647 <div class="vn-progress-track"> 5648 <div class="vn-progress-bar"></div> 5649 </div> 5650 </div> 5651 <div class="vn-time-row"> 5652 <span class="vn-elapsed">0:00</span> 5653 <span class="vn-remaining">0:00</span> 5654 </div> 5655 </div> 5656 </div>`; 5657 } else if (post.media_type === 'video') { 5658 mediaHtml = `<div class="post-media"><video controls controlslist="nodownload nofullscreen noremoteplayback" disablepictureinpicture oncontextmenu="return false;" preload="metadata" src="${post.media_url}#t=0.001"></video></div>`; 5659 } else { 5660 mediaHtml = `<div class="post-media"><img src="${post.media_url}" alt="Post media" oncontextmenu="return false;" draggable="false"></div>`; 5661 } 5662 } 5663 5664 // Build reactions strip + picker 5665 const postReactionsHtml = renderReactionsRow(post.post_reactions || [], 'post', post.id); 5666 5667 // Don't show the placeholder body for voice-note-only posts 5668 const showBody = post.body && post.body !== '(voice note)'; 5669 5670 let html = ` 5671 <div class="post-card" data-post-id="${post.id}" data-post-type="${post.post_type}" data-hidden="${!!post.hidden_from_feed}"> 5672 <div class="post-type-label">${typeLabel}</div> 5673 ${post.title ? `<div class="post-title">${escapeHtml(post.title)}</div>` : ''} 5674 ${showBody ? `<div class="post-body">${escapeHtml(post.body)}</div>` : ''} 5675 ${mediaHtml} 5676 ${postReactionsHtml} 5677 <div class="post-meta"> 5678 <div class="post-author"> 5679 <span class="post-author-name">${escapeHtml(authorName)}</span> 5680 <span class="post-date">${date}</span> 5681 </div> 5682 <div style="display:flex;align-items:center;gap:8px;"> 5683 ${isOwner ? `<button class="post-edit-btn" onclick="editPost('${post.id}', this)">Edit</button>` : ''} 5684 ${isCurrentTeacher ? `<button class="post-hide-btn" onclick="toggleHideFromFeed('${post.id}', ${!!post.hidden_from_feed})">${post.hidden_from_feed ? '◆ Show in Feed' : '◇ Hide from Feed'}</button>` : ''} 5685 <button class="heart-btn ${isHearted ? 'hearted' : ''}" onclick="toggleHeart('${post.id}', 'post')" aria-label="${isHearted ? 'Remove heart' : 'Send heart'}" aria-pressed="${isHearted}"> 5686 <span class="heart-icon" aria-hidden="true">${isHearted ? '♥' : '♡'}</span> 5687 <span class="heart-count" aria-label="${heartCount} hearts">${heartCount || ''}</span> 5688 </button> 5689 ${(isOwner || isCurrentTeacher) ? `<button class="post-delete" onclick="deletePost('${post.id}')" title="Delete">×</button>` : ''} 5690 </div> 5691 </div> 5692 5693 <!-- Comments --> 5694 <div class="reflections-section"> 5695 <div class="reflections-bar"> 5696 <button class="reflections-toggle" onclick="toggleReflections('${post.id}')"> 5697 ${reflections.length} ${reflections.length === 1 ? 'comment' : 'comments'} ▾ 5698 </button> 5699 <button class="reply-btn" onclick="openReply('${post.id}')">Reply</button> 5700 </div> 5701 <div class="reflections-list" id="reflections-${post.id}" style="display:none;"> 5702 ${(() => { 5703 const COMMENT_LIMIT = 10; 5704 if (reflections.length <= COMMENT_LIMIT) { 5705 return reflections.map(r => renderReflection(r)).join(''); 5706 } 5707 const hiddenCount = reflections.length - COMMENT_LIMIT; 5708 const visible = reflections.slice(-COMMENT_LIMIT); 5709 const hidden = reflections.slice(0, hiddenCount); 5710 return ` 5711 <button class="show-more-comments" onclick="showAllComments('${post.id}')" id="show-more-${post.id}"> 5712 ▾ Show ${hiddenCount} earlier comment${hiddenCount === 1 ? '' : 's'} 5713 </button> 5714 <div id="hidden-comments-${post.id}" style="display:none;">${hidden.map(r => renderReflection(r)).join('')}</div> 5715 ${visible.map(r => renderReflection(r)).join('')}`; 5716 })()} 5717 <div class="reflection-composer"> 5718 <textarea class="reflection-textarea" id="reflection-input-${post.id}" placeholder="Write a comment..."></textarea> 5719 <div id="comment-media-preview-${post.id}" class="media-preview" style="display:none;"></div> 5720 <input type="file" id="comment-media-input-${post.id}" accept="image/*,video/*" style="display:none;" onchange="handleCommentMediaSelect(this, '${post.id}')"> 5721 <div style="display:flex;align-items:center;justify-content:space-between;margin-top:6px;"> 5722 <div style="display:flex;align-items:center;gap:4px;"> 5723 <button class="comment-media-btn" onclick="document.getElementById('comment-media-input-${post.id}').click()" title="Add photo or video">📷</button> 5724 <div style="position:relative;display:inline-block;"> 5725 <button class="comment-media-btn comment-emoji-btn" onclick="toggleEmojiPicker('reflection-input-${post.id}')" title="Add emoji">😊</button> 5726 <div class="emoji-picker" id="emoji-picker-reflection-input-${post.id}" style="display:none;"></div> 5727 </div> 5728 </div> 5729 <button class="reflection-submit" onclick="submitReflection('${post.id}')">Share</button> 5730 </div> 5731 </div> 5732 </div> 5733 </div> 5734 </div> 5735 <div class="feed-divider"><span>♦︎</span></div>`; 5736 5737 return html; 5738} 5739 5740async function toggleHideFromFeed(postId, currentlyHidden) { 5741 if (!sbClient) return; 5742 const { error } = await sbClient 5743 .from('posts') 5744 .update({ hidden_from_feed: !currentlyHidden }) 5745 .eq('id', postId); 5746 if (error) { 5747 console.error('Error toggling hide from feed:', error); 5748 return; 5749 } 5750 const activeView = window.location.hash.replace('#', '') || 'home'; 5751 if (activeView === 'teachings') { 5752 loadTeachings(); 5753 } else { 5754 loadFeed(); 5755 } 5756} 5757 5758function renderReflection(r) { 5759 const name = r.author?.display_name || 'Sister'; 5760 const date = new Date(r.created_at).toLocaleDateString('en-US', { month: 'short', day: 'numeric' }); 5761 const isOwner = currentUser && r.author_id === currentUser.id; 5762 const isCurrentTeacher = currentProfile && currentProfile.role === 'teacher'; 5763 5764 // Hearts on comments 5765 const rHearts = r.hearts || []; 5766 const rHeartCount = rHearts.length; 5767 const rIsHearted = rHearts.some(h => h.user_id === currentUser?.id); 5768 5769 let rMediaHtml = ''; 5770 if (r.media_url) { 5771 if (r.media_type === 'video') { 5772 rMediaHtml = `<div class="post-media" style="margin:8px 0;"><video controls controlslist="nodownload nofullscreen noremoteplayback" disablepictureinpicture oncontextmenu="return false;" preload="metadata" src="${r.media_url}#t=0.001" style="max-height:200px;"></video></div>`; 5773 } else { 5774 rMediaHtml = `<div class="post-media" style="margin:8px 0;"><img src="${r.media_url}" alt="" style="max-height:200px;" oncontextmenu="return false;" draggable="false"></div>`; 5775 } 5776 } 5777 5778 // Emoji reactions on this reflection 5779 const reflectionReactionsHtml = renderReactionsRow(r.reflection_reactions || [], 'reflection', r.id); 5780 5781 return ` 5782 <div class="reflection-card" data-reflection-id="${r.id}"> 5783 <div class="reflection-body">${escapeHtml(r.body)}</div> 5784 ${rMediaHtml} 5785 ${reflectionReactionsHtml} 5786 <div class="reflection-meta">
5787 <span class="reflection-author">${escapeHtml(name)}</span> 5788 <span style="display:flex;align-items:center;gap:8px;"> 5789 <button class="heart-btn ${rIsHearted ? 'hearted' : ''}" onclick="toggleHeart('${r.id}', 'reflection')" style="font-size:12px;padding:2px 4px;" aria-label="${rIsHearted ? 'Remove heart' : 'Send heart'}" aria-pressed="${rIsHearted}"> 5790 <span class="heart-icon" aria-hidden="true">${rIsHearted ? '♥' : '♡'}</span> 5791 <span class="heart-count" aria-label="${rHeartCount} hearts">${rHeartCount > 0 ? rHeartCount : ''}</span> 5792 </button> 5793 <button class="post-edit-btn" onclick="replyToComment('${r.post_id}', '${escapeHtml(name)}')" style="font-size:8px;">Reply</button> 5794 ${isOwner ? `<button class="post-edit-btn" onclick="editComment('${r.id}', '${r.post_id}', this)" style="font-size:8px;">Edit</button>` : ''} 5795 <span class="reflection-date">${date}</span> 5796 ${(isOwner || isCurrentTeacher) ? `<button class="post-delete" onclick="deleteReflection('${r.id}', '${r.post_id}')" title="Delete" style="font-size:12px;">×</button>` : ''} 5797 </span> 5798 </div> 5799 </div>`; 5800} 5801 5802// Shared reactions renderer for posts and reflections 5803const SISTER_EMOJIS = ['â¤ï¸', 'â¨', 'ð', 'ð', 'ð¤']; 5804 5805function renderReactionsRow(reactions, type, targetId) { 5806 // type is 'post' or 'reflection' â used to call the right toggle function 5807 const fnName = type === 'post' ? 'togglePostReaction' : 'toggleReflectionReaction'; 5808 5809 // Group by emoji 5810 const grouped = {}; 5811 reactions.forEach(r => { 5812 if (!grouped[r.emoji]) grouped[r.emoji] = []; 5813 grouped[r.emoji].push(r); 5814 }); 5815 5816 const chips = Object.keys(grouped).map(emoji => { 5817 const list = grouped[emoji]; 5818 const mine = currentUser && list.some(r => r.user_id === currentUser.id); 5819 return `<button class="sister-reaction-chip ${mine ? 'mine' : ''}" onclick="${fnName}('${targetId}','${emoji}')" aria-label="${list.length} ${emoji} reaction${list.length === 1 ? '' : 's'}${mine ? ', tap to remove yours' : ', tap to add yours'}" aria-pressed="${mine}"><span aria-hidden="true">${emoji}</span><span class="sister-reaction-count" aria-hidden="true">${list.length}</span></button>`; 5820 }).join(''); 5821 5822 const picker = `<span class="sister-reaction-picker" onclick="event.stopPropagation();">${SISTER_EMOJIS.map(em => `<button class="sister-reaction-add" onclick="${fnName}('${targetId}','${em}')" aria-label="React with ${em}"><span aria-hidden="true">${em}</span></button>`).join('')}</span>`; 5823 5824 return `<div class="sister-reactions-row">${chips}${picker}</div>`; 5825} 5826 5827async function togglePostReaction(postId, emoji) { 5828 if (!sbClient || !currentUser) return; 5829 const { data: existing } = await sbClient 5830 .from('post_reactions') 5831 .select('id') 5832 .eq('post_id', postId) 5833 .eq('user_id', currentUser.id) 5834 .eq('emoji', emoji); 5835 if (existing && existing.length > 0) { 5836 await sbClient.from('post_reactions').delete().eq('id', existing[0].id); 5837 } else { 5838 await sbClient.from('post_reactions').insert({ post_id: postId, user_id: currentUser.id, emoji }); 5839 } 5840 await loadFeed(); 5841} 5842 5843async function toggleReflectionReaction(reflectionId, emoji) { 5844 if (!sbClient || !currentUser) return; 5845 const { data: existing } = await sbClient 5846 .from('reflection_reactions') 5847 .select('id') 5848 .eq('reflection_id', reflectionId) 5849 .eq('user_id', currentUser.id) 5850 .eq('emoji', emoji); 5851 if (existing && existing.length > 0) { 5852 await sbClient.from('reflection_reactions').delete().eq('id', existing[0].id); 5853 } else { 5854 await sbClient.from('reflection_reactions').insert({ reflection_id: reflectionId, user_id: currentUser.id, emoji }); 5855 } 5856 await loadFeed(); 5857} 5858 5859function toggleReflections(postId) { 5860 const el = document.getElementById('reflections-' + postId); 5861 el.style.display = el.style.display === 'none' ? 'block' : 'none'; 5862} 5863 5864function showAllComments(postId) { 5865 const hidden = document.getElementById('hidden-comments-' + postId); 5866 const btn = document.getElementById('show-more-' + postId); 5867 if (hidden) hidden.style.display = 'block'; 5868 if (btn) btn.style.display = 'none'; 5869} 5870 5871function toggleComposer() { 5872 const expanded = document.getElementById('composer-expanded'); 5873 expanded.classList.toggle('open'); 5874 if (expanded.classList.contains('open')) { 5875 document.getElementById('compose-body').focus(); 5876 } 5877} 5878 5879function closeComposer() { 5880 document.getElementById('composer-expanded').classList.remove('open'); 5881 document.getElementById('compose-title').value = ''; 5882 document.getElementById('compose-body').value = ''; 5883 clearMediaPreview(); 5884} 5885 5886function openReply(postId) { 5887 // Open the comments section and focus the reply input 5888 const el = document.getElementById('reflections-' + postId); 5889 el.style.display = 'block'; 5890 const input = document.getElementById('reflection-input-' + postId); 5891 if (input) input.focus(); 5892} 5893 5894function replyToComment(postId, authorName) { 5895 // Open comments, pre-fill with @name 5896 const el = document.getElementById('reflections-' + postId); 5897 el.style.display = 'block'; 5898 const input = document.getElementById('reflection-input-' + postId); 5899 if (input) { 5900 input.value = '@' + authorName + ' '; 5901 input.focus(); 5902 } 5903} 5904 5905// Demo feed when Supabase isn't configured 5906function renderDemoFeed() { 5907 return ` 5908 <div class="post-card"> 5909 <div class="post-type-label">Teaching</div> 5910 <div class="post-title">
5910Welcome to Your Private Circle</div> 5911 <div class="post-body">This is your sacred community space. Once Supabase is connected, you'll be able to post teachings, and your students will share comments right here in real-time. 5912 5913Each teaching you share will appear as an elegant card. Students respond beneath with their own comments. A gold heart lets them quietly say: I see you. I'm with you. 5914 5915This is not social media. This is sitting in circle.</div> 5916 <div class="post-meta"> 5917 <div class="post-author"> 5918 <span class="post-author-name">Sumir</span> 5919 <span class="post-date">April 15, 2026</span> 5920 </div> 5921 <div> 5922 <button class="heart-btn hearted"> 5923 <span class="heart-icon">♥</span> 5924 <span class="heart-count">3</span> 5925 </button> 5926 </div> 5927 </div> 5928 <div class="reflections-section"> 5929 <button class="reflections-toggle" onclick="this.nextElementSibling.style.display = this.nextElementSibling.style.display === 'none' ? 'block' : 'none'"> 5930 2 comments ▾ 5931 </button> 5932 <div class="reflections-list" style="display:none;"> 5933 <div class="reflection-card"> 5934 <div class="reflection-body">This space already feels different. Like a room that was waiting for us. Thank you for creating this.</div> 5935 <div class="reflection-meta"> 5936 <span class="reflection-author">Sarah</span> 5937 <span class="reflection-date">Apr 15</span> 5938 </div> 5939 </div> 5940 <div class="reflection-card"> 5941 <div class="reflection-body">I felt something shift just reading this. Looking forward to what unfolds here.</div> 5942 <div class="reflection-meta"> 5943 <span class="reflection-author">Maria</span> 5944 <span class="reflection-date">Apr 15</span> 5945 </div> 5946 </div> 5947 </div> 5948 </div> 5949 </div> 5950 <div class="feed-divider"><span>♦︎</span></div>`; 5951} 5952 5953// ============================================================ 5954// FEED FILTER & SEARCH 5955// ============================================================ 5956let currentFilter = 'all'; 5957let currentSearch = ''; 5958 5959function filterFeed(type) { 5960 currentFilter = type; 5961 // Update active button 5962 document.querySelectorAll('.feed-filter-btn').forEach(btn => {
5963 btn.classList.toggle('active', btn.dataset.filter === type); 5964 }); 5965 applyFeedFilters(); 5966} 5967 5968function searchFeed(query) { 5969 currentSearch = query.toLowerCase().trim(); 5970 const clearBtn = document.getElementById('feed-search-clear'); 5971 clearBtn.classList.toggle('show', currentSearch.length > 0); 5972 applyFeedFilters(); 5973} 5974 5975function clearSearch() { 5976 document.getElementById('feed-search-input').value = ''; 5977 currentSearch = ''; 5978 document.getElementById('feed-search-clear').classList.remove('show'); 5979 applyFeedFilters(); 5980} 5981 5982function applyFeedFilters() { 5983 const posts = document.querySelectorAll('#feed-container .post-card'); 5984 const dividers = document.querySelectorAll('#feed-container .feed-divider'); 5985 let visibleCount = 0; 5986 5987 posts.forEach((post, i) => { 5988 const postType = post.dataset.postType || ''; 5989 const title = post.querySelector('.post-title'); 5990 const body = post.querySelector('.post-body'); 5991 const author = post.querySelector('.post-author-name'); 5992 5993 // Check filter 5994 const isHidden = post.dataset.hidden === 'true'; 5995 let matchesFilter = false; 5996 if (currentFilter === 'hidden') { 5997 matchesFilter = isHidden; 5998 } else if (currentFilter === 'all') { 5999 matchesFilter = !isHidden; 6000 } else { 6001 matchesFilter = !isHidden && postType === currentFilter; 6002 } 6003 6004 // Check search 6005 let matchesSearch = true; 6006 if (currentSearch) { 6007 const searchText = [ 6008 title ? title.textContent : '', 6009 body ? body.textContent : '', 6010 author ? author.textContent : '' 6011 ].join(' ').toLowerCase(); 6012 6013 // Also search reflections within the post 6014 const reflections = post.querySelectorAll('.reflection-body'); 6015 reflections.forEach(r => { searchText + ' ' + r.textContent.toLowerCase(); }); 6016 6017 matchesSearch = searchText.includes(currentSearch); 6018 } 6019 6020 const visible = matchesFilter && matchesSearch; 6021 post.style.display = visible ? '' : 'none'; 6022 if (dividers[i]) dividers[i].style.display = visible ? '' : 'none'; 6023 if (visible) visibleCount++; 6024 }); 6025 6026 // Show/hide empty state 6027 const emptyEls = document.querySelectorAll('#feed-container .feed-empty'); 6028 if (visibleCount === 0 && posts.length > 0) { 6029 // All posts filtered out â show a message 6030 let noResults = document.getElementById('feed-no-results'); 6031 if (!noResults) { 6032 noResults = document.createElement('div'); 6033 noResults.id = 'feed-no-results'; 6034 noResults.className = 'feed-empty'; 6035 noResults.innerHTML = '<div class="feed-empty-icon">✦</div><p class="feed-empty-text" style="font-style:italic;">No posts match your search.</p>'; 6036 document.getElementById('feed-container').appendChild(noResults); 6037 } 6038 noResults.style.display = ''; 6039 } else { 6040 const noResults = document.getElementById('feed-no-results'); 6041 if (noResults) noResults.style.display = 'none'; 6042 } 6043} 6044 6045// ============================================================ 6046// MEDIA UPLOAD 6047// ============================================================ 6048let pendingMediaFile = null; 6049let pendingCommentMedia = {}; // keyed by postId 6050let pendingVoiceNoteFile = null; // holds the recorded voice note Blob until posted 6051 6052// ============================================================ 6053// VOICE NOTE RECORDER (5 min max) 6054// ============================================================ 6055const VOICE_NOTE_MAX_SECONDS = 300; // 5 minutes 6056let vrMediaRecorder = null; 6057let vrChunks = []; 6058let vrStartTime = 0; 6059let vrTimerInterval = null; 6060let vrStream = null; 6061// MP3 recorder state (Web Audio + lamejs â plays on every browser incl. iOS) 6062let vrAudioCtx = null; 6063let vrSourceNode = null; 6064let vrProcessor = null; 6065let vrMp3Encoder = null; 6066let vrMp3Data = []; 6067let vrRecording = false; 6068let vrRecordedBlob = null; 6069 6070function openVoiceRecorder() { 6071 // Reset state 6072 vrChunks = []; 6073 vrRecordedBlob = null; 6074 document.getElementById('vr-mic-icon').innerHTML = '🎤'; 6075 document.getElementById('vr-mic-btn').classList.remove('recording'); 6076 document.getElementById('vr-title').textContent = 'Speak from the heart'; 6077 document.getElementById('vr-subtitle').textContent = 'Tap the microphone to begin â up to 5 minutes'; 6078 document.getElementById('vr-timer').textContent = '0:00'; 6079 document.getElementById('vr-hint').textContent = '5 minutes maximum'; 6080 document.getElementById('vr-preview').style.display = 'none'; 6081 document.getElementById('vr-rerecord').style.display = 'none'; 6082 document.getElementById('vr-use').style.display = 'none'; 6083 document.getElementById('voice-recorder-modal').style.display = 'flex'; 6084} 6085 6086function vrTeardownGraph() { 6087 // Disconnect the Web Audio graph and release the microphone 6088 try { if (vrProcessor) { vrProcessor.disconnect(); vrProcessor.onaudioprocess = null; } } catch (e) {} 6089 try { if (vrSourceNode) vrSourceNode.disconnect(); } catch (e) {} 6090 vrProcessor = null; 6091 vrSourceNode = null; 6092 if (vrStream) {
6093 vrStream.getTracks().forEach(t => t.stop()); 6094 vrStream = null; 6095 } 6096 vrRecording = false; 6097} 6098 6099function closeVoiceRecorder() { 6100 // Stop any active recording / release mic 6101 if (vrRecording) { try { stopMp3Recording(); } catch (e) {} } 6102 vrTeardownGraph(); 6103 if (vrTimerInterval) { clearInterval(vrTimerInterval); vrTimerInterval = null; } 6104 document.getElementById('voice-recorder-modal').style.display = 'none'; 6105} 6106 6107function resetVoiceRecorder() { 6108 vrChunks = []; 6109 vrRecordedBlob = null; 6110 openVoiceRecorder(); 6111} 6112 6113async function toggleRecording() { 6114 if (vrRecording) { 6115 stopMp3Recording(); 6116 return; 6117 } 6118 6119 // Encoder must be available (lame.min.js loaded) 6120 if (typeof lamejs === 'undefined' || !lamejs.Mp3Encoder) { 6121 showToast('Recorder is still loading â please try again in a moment.', 'error'); 6122 return; 6123 } 6124 6125 // Create/resume the AudioContext synchronously inside this click so iOS 6126 // Safari allows microphone capture (it loses the user gesture after await). 6127 try { 6128 if (!vrAudioCtx) { 6129 vrAudioCtx = new (window.AudioContext || window.webkitAudioContext)(); 6130 } 6131 if (vrAudioCtx.state === 'suspended') { await vrAudioCtx.resume(); } 6132 } catch (e) { 6133 showToast('Audio is not supported on this browser.', 'error'); 6134 return; 6135 } 6136 6137 // Request the microphone 6138 try { 6139 vrStream = await navigator.mediaDevices.getUserMedia({ 6140 audio: { echoCancellation: true, noiseSuppression: true, channelCount: 1 } 6141 }); 6142 } catch (e) { 6143 showToast('Microphone access denied. Please allow microphone permission and try again.', 'error'); 6144 return; 6145 } 6146 6147 const sampleRate = vrAudioCtx.sampleRate; // typically 44100 or 48000 6148 vrMp3Encoder = new lamejs.Mp3Encoder(1, sampleRate, 96); // mono, 96kbps â small + clear for voice 6149 vrMp3Data = []; 6150 6151 vrSourceNode = vrAudioCtx.createMediaStreamSource(vrStream); 6152 // ScriptProcessorNode has the widest support (incl. older iOS) for raw PCM capture 6153 vrProcessor = vrAudioCtx.createScriptProcessor(4096, 1, 1); 6154 6155 vrProcessor.onaudioprocess = (ev) => { 6156 if (!vrRecording) return; 6157 const input = ev.inputBuffer.getChannelData(0); // Float32 [-1,1] 6158 const samples = new Int16Array(input.length); 6159 for (let i = 0; i < input.length; i++) { 6160 let s = Math.max(-1, Math.min(1, input[i])); 6161 samples[i] = s < 0 ? s * 0x8000 : s * 0x7FFF; 6162 } 6163 const mp3buf = vrMp3Encoder.encodeBuffer(samples); 6164 if (mp3buf.length > 0) vrMp3Data.push(new Int8Array(mp3buf)); 6165 }; 6166 6167 vrSourceNode.connect(vrProcessor); 6168 // Connect to destination through a muted gain so the graph runs without echo 6169 const silentGain = vrAudioCtx.createGain(); 6170 silentGain.gain.value = 0; 6171 vrProcessor.connect(silentGain); 6172 silentGain.connect(vrAudioCtx.destination); 6173 6174 vrRecording = true; 6175 vrStartTime = Date.now(); 6176 document.getElementById('vr-mic-btn').classList.add('recording'); 6177 document.getElementById('vr-mic-icon').innerHTML = '■'; // stop square 6178 document.getElementById('vr-title').textContent = 'Recordingâ¦'; 6179 document.getElementById('vr-subtitle').textContent = 'Tap again to stop'; 6180 6181 vrTimerInterval = setInterval(() => { 6182 const elapsed = Math.floor((Date.now() - vrStartTime) / 1000); 6183 document.getElementById('vr-timer').textContent = formatDuration(elapsed); 6184 if (elapsed >= VOICE_NOTE_MAX_SECONDS) { 6185 stopMp3Recording(); 6186 showToast('Voice note reached the 5-minute maximum.', 'success'); 6187 } 6188 }, 300); 6189} 6190 6191function stopMp3Recording() { 6192 if (!vrRecording) return; 6193 vrRecording = false; 6194 if (vrTimerInterval) { clearInterval(vrTimerInterval); vrTimerInterval = null; } 6195 6196 // Flush any remaining MP3 data 6197 try { 6198 const end = vrMp3Encoder.flush(); 6199 if (end.length > 0) vrMp3Data.push(new Int8Array(end)); 6200 } catch (e) {} 6201 6202 // Stop the graph + mic 6203 vrTeardownGraph(); 6204 6205 // Assemble the final MP3 blob 6206 vrRecordedBlob = new Blob(vrMp3Data, { type: 'audio/mpeg' }); 6207 vrMp3Data = []; 6208 vrMp3Encoder = null; 6209 6210 const url = URL.createObjectURL(vrRecordedBlob); 6211 const playback = document.getElementById('vr-playback'); 6212 playback.src = url; 6213 document.getElementById('vr-preview').style.display = 'block'; 6214 document.getElementById('vr-mic-btn').classList.remove('recording'); 6215 document.getElementById('vr-mic-icon').innerHTML = '🎤'; 6216 document.getElementById('vr-title').textContent = 'Listen back'; 6217 document.getElementById('vr-subtitle').textContent = 'Use this recording, or re-record.'; 6218 document.getElementById('vr-rerecord').style.display = 'inline-block'; 6219 document.getElementById('vr-use').style.display = 'inline-block'; 6220} 6221 6222function formatDuration(seconds) { 6223 const m = Math.floor(seconds / 60); 6224 const s = seconds % 60; 6225 return m + ':' + (s < 10 ? '0' : '') + s; 6226} 6227 6228function attachVoiceNote() { 6229 if (!vrRecordedBlob) return; 6230 6231 // Turn the Blob into a File so uploadMedia can handle it (always MP3 now) 6232 const filename = `voice-note-${Date.now()}.mp3`; 6233 pendingVoiceNoteFile = new File([vrRecordedBlob], filename, { type: 'audio/mpeg' }); 6234 6235 // Show the preview row in the composer 6236 const previewEl = document.getElementById('voice-note-preview'); 6237 const audioEl = document.getElementById('voice-note-audio'); 6238 audioEl.src = URL.createObjectURL(vrRecordedBlob); 6239 const durationEl = document.getElementById('voice-note-duration'); 6240 6241 audioEl.onloadedmetadata = () => { 6242 const dur = isFinite(audioEl.duration) ? Math.round(audioEl.duration) : 0; 6243 durationEl.textContent = 'Voice note attached · ' + formatDuration(dur); 6244 // Force re-render 6245 durationEl.innerHTML = durationEl.textContent; 6246 }; 6247 previewEl.style.display = 'block'; 6248 6249 closeVoiceRecorder(); 6250 showToast('Voice note attached. Add a title or text if you\'d like, then tap Share.', 'success'); 6251} 6252 6253function clearVoiceNote() { 6254 pendingVoiceNoteFile = null; 6255 document.getElementById('voice-note-preview').style.display = 'none'; 6256 document.getElementById('voice-note-audio').src = ''; 6257 document.getElementById('voice-note-duration').textContent = ''; 6258} 6259 6260async function uploadMedia(file) { 6261 const ext = file.name.split('.').pop(); 6262 const path = `${currentUser.id}/${Date.now()}.${ext}`; 6263 6264 const { data, error } = await sbClient.storage 6265 .from('Media') 6266 .upload(path, file, { 6267 cacheControl: '3600', 6268 upsert: false 6269 }); 6270 6271 if (error) { 6272 console.error('Upload error:', error); 6273 showToast('Media upload failed: ' + error.message, 'error'); 6274 return null; 6275 } 6276 6277 const { data: urlData } = sbClient.storage 6278 .from('Media') 6279 .getPublicUrl(path); 6280 6281 console.log('Upload success. URL:', urlData.publicUrl); 6282 6283 return { 6284 url: urlData.publicUrl, 6285 type: file.type.startsWith('audio') 6286 ? 'audio' 6287 : (file.type.startsWith('video') ? 'video' : 'image') 6288 }; 6289} 6290 6291function handleMediaSelect(input) { 6292 const file = input.files[0]; 6293 if (!file) return; 6294 6295 pendingMediaFile = file; 6296 const preview = document.getElementById('compose-media-preview'); 6297 preview.style.display = 'block'; 6298 6299 if (file.type.startsWith('video')) { 6300 const url = URL.createObjectURL(file); 6301 preview.innerHTML = `<video src="${url}" style="max-height:200px;border-radius:8px;" controls></video><button class="media-preview-remove" onclick="clearMediaPreview()">×</button>`; 6302 } else { 6303 const reader = new FileReader(); 6304 reader.onload = (e) => { 6305 preview.innerHTML = `<img src="${e.target.result}"><button class="media-preview-remove" onclick="clearMediaPreview()">×</button>`; 6306 };
6307 reader.readAsDataURL(file); 6308 } 6309} 6310 6311function clearMediaPreview() { 6312 pendingMediaFile = null; 6313 const preview = document.getElementById('compose-media-preview'); 6314 preview.style.display = 'none'; 6315 preview.innerHTML = ''; 6316 document.getElementById('compose-media-input').value = ''; 6317} 6318 6319function handleCommentMediaSelect(input, postId) { 6320 const file = input.files[0]; 6321 if (!file) return; 6322 6323 pendingCommentMedia[postId] = file; 6324 const preview = document.getElementById('comment-media-preview-' + postId); 6325 preview.style.display = 'block'; 6326 6327 if (file.type.startsWith('video')) { 6328 const url = URL.createObjectURL(file); 6329 preview.innerHTML = `<video src="${url}" style="max-height:150px;border-radius:8px;" controls></video><button class="media-preview-remove" onclick="clearCommentMedia('${postId}')">×</button>`; 6330 } else { 6331 const reader = new FileReader(); 6332 reader.onload = (e) => { 6333 preview.innerHTML = `<img src="${e.target.result}" style="max-height:150px;"><button class="media-preview-remove" onclick="clearCommentMedia('${postId}')">×</button>`; 6334 }; 6335 reader.readAsDataURL(file); 6336 } 6337} 6338 6339function clearCommentMedia(postId) { 6340 delete pendingCommentMedia[postId]; 6341 const preview = document.getElementById('comment-media-preview-' + postId); 6342 preview.style.display = 'none'; 6343 preview.innerHTML = ''; 6344 document.getElementById('comment-media-input-' + postId).value = ''; 6345} 6346 6347// ============================================================ 6348// FEED ACTIONS 6349// ============================================================ 6350async function createPost() { 6351 if (!sbClient || !currentUser) return; 6352 6353 const title = document.getElementById('compose-title').value.trim(); 6354 const body = document.getElementById('compose-body').value.trim(); 6355 const type = document.getElementById('compose-type').value; 6356 6357 // Voice notes can post without typed body text 6358 const hasVoiceNote = !!pendingVoiceNoteFile; 6359 if (!body && !hasVoiceNote) return; 6360 6361 const btn = document.getElementById('compose-submit'); 6362 btn.disabled = true; 6363 btn.textContent = 'Sharing...'; 6364 6365 try { 6366 let mediaUrl = null; 6367 let mediaType = null; 6368 6369 // Voice note takes priority (it's a different kind of media) 6370 if (hasVoiceNote) { 6371 const result = await uploadMedia(pendingVoiceNoteFile); 6372 if (result) { 6373 mediaUrl = result.url; 6374 mediaType = 'audio'; 6375 } else { 6376 btn.disabled = false; 6377 btn.textContent = 'Share'; 6378 return; 6379 } 6380 } else if (pendingMediaFile) { 6381 const result = await uploadMedia(pendingMediaFile); 6382 if (result) { 6383 mediaUrl = result.url; 6384 mediaType = result.type; 6385 } else { 6386 btn.disabled = false; 6387 btn.textContent = 'Share'; 6388 return; 6389 } 6390 } 6391 6392 const postData = { 6393 author_id: currentUser.id, 6394 title: title || null, 6395 // If no typed body but voice note present, supply a placeholder so the NOT NULL column is happy 6396 body: body || (hasVoiceNote ? '(voice note)' : ''), 6397 // Mark as voice_note so the feed renders it specially 6398 post_type: hasVoiceNote ? 'voice_note' : type 6399 }; 6400 if (mediaUrl) { 6401 postData.media_url = mediaUrl; 6402 postData.media_type = mediaType; 6403 } 6404 6405 const { error } = await sbClient.from('posts').insert(postData); 6406 6407 if (error) { 6408 console.error('Post error:', error); 6409 showToast('Could not share: ' + error.message, 'error'); 6410 } else { 6411 document.getElementById('compose-title').value = ''; 6412 document.getElementById('compose-body').value = ''; 6413 clearMediaPreview(); 6414 clearVoiceNote(); 6415 closeComposer(); 6416 await loadFeed(); 6417 } 6418 } catch (e) { 6419 console.error('Post exception:', e); 6420 showToast('Something went wrong: ' + e.message, 'error'); 6421 } 6422 6423 btn.disabled = false; 6424 document.getElementById('compose-submit').textContent = 'Share'; 6425} 6426 6427async function deletePost(postId) { 6428 if (!sbClient) return; 6429 showConfirmModal('Remove this post?', async () => { 6430 await sbClient.from('posts').delete().eq('id', postId); 6431 await loadFeed(); 6432 }); 6433} 6434 6435async function submitReflection(postId) { 6436 if (!sbClient || !currentUser) return; 6437 6438 const input = document.getElementById('reflection-input-' + postId); 6439 const body = input.value.trim(); 6440 if (!body) return; 6441 6442 // Find the Share button next to this input 6443 const btn = input.parentElement?.querySelector('.reflection-submit') || input.closest('.reflection-input-row')?.querySelector('.reflection-submit'); 6444 setButtonLoading(btn, true, 'Sharing...'); 6445 6446 try { 6447 let mediaUrl = null; 6448 let mediaType = null; 6449 6450 if (pendingCommentMedia[postId]) { 6451 const result = await uploadMedia(pendingCommentMedia[postId]); 6452 if (result) { 6453 mediaUrl = result.url; 6454 mediaType = result.type; 6455 } 6456 clearCommentMedia(postId); 6457 } 6458 6459 const commentData = { 6460 post_id: postId, 6461 author_id: currentUser.id, 6462 body 6463 }; 6464 if (mediaUrl) { 6465 commentData.media_url = mediaUrl; 6466 commentData.media_type = mediaType; 6467 } 6468 6469 const { error } = await sbClient.from('reflections').insert(commentData); 6470 if (error) throw error; 6471 6472 input.value = ''; 6473 await loadFeed(); 6474 setTimeout(() => { 6475 const el = document.getElementById('reflections-' + postId); 6476 if (el) el.style.display = 'block'; 6477 }, 100); 6478 } catch (e) { 6479 showToast('Could not share comment: ' + (e.message || e), 'error'); 6480 } finally { 6481 setButtonLoading(btn, false); 6482 } 6483} 6484 6485// ============================================================ 6486// EDIT POSTS & COMMENTS 6487// ============================================================ 6488// Branded toast notification (replaces alert) 6489function showToast(message, type) { 6490 let container = document.getElementById('fr-toast-container'); 6491 if (!container) { 6492 container = document.createElement('div'); 6493 container.id = 'fr-toast-container'; 6494 container.className = 'fr-toast-container'; 6495 document.body.appendChild(container); 6496 } 6497 const toast = document.createElement('div'); 6498 toast.className = 'fr-toast ' + (type || ''); 6499 toast.textContent = message; 6500 container.appendChild(toast); 6501 // Animate in 6502 requestAnimationFrame(() => toast.classList.add('show')); 6503 // Auto-dismiss 6504 setTimeout(() => { 6505 toast.classList.remove('show'); 6506 setTimeout(() => toast.remove(), 350); 6507 }, 4200); 6508} 6509 6510// Toggle a button into a loading state and back 6511function setButtonLoading(btn, loading, loadingText) { 6512 if (!btn) return; 6513 if (loading) { 6514 btn.disabled = true; 6515 if (!btn.dataset.originalText) btn.dataset.originalText = btn.textContent; 6516 btn.textContent = loadingText || 'One moment...'; 6517 btn.classList.add('is-loading'); 6518 } else { 6519 btn.disabled = false;
6520 if (btn.dataset.originalText) { 6521 btn.textContent = btn.dataset.originalText; 6522 delete btn.dataset.originalText; 6523 } 6524 btn.classList.remove('is-loading'); 6525 } 6526} 6527 6528function showConfirmModal(message, onConfirm) { 6529 const overlay = document.createElement('div'); 6530 overlay.className = 'edit-overlay'; 6531 overlay.innerHTML = ` 6532 <div class="edit-modal" style="text-align:center;max-width:360px;"> 6533 <div style="font-size:28px;color:var(--gold);margin-bottom:14px;">✦</div> 6534 <p style="font-family:'Cormorant Garamond',serif;font-size:18px;color:var(--charcoal);margin-bottom:24px;">${message}</p> 6535 <div class="edit-modal-actions" style="justify-content:center;"> 6536 <button class="edit-modal-cancel" onclick="this.closest('.edit-overlay').remove()">Cancel</button> 6537 <button class="edit-modal-save" id="confirm-yes-btn" style="background:var(--berry);">Remove</button> 6538 </div> 6539 </div>`; 6540 document.body.appendChild(overlay); 6541 overlay.querySelector('#confirm-yes-btn').addEventListener('click', async () => { 6542 overlay.remove(); 6543 await onConfirm(); 6544 }); 6545} 6546 6547function editPost(postId, btn) { 6548 const card = btn.closest('.post-card'); 6549 const bodyEl = card.querySelector('.post-body'); 6550 const currentText = bodyEl.textContent; 6551 6552 const overlay = document.createElement('div'); 6553 overlay.className = 'edit-overlay'; 6554 overlay.innerHTML = ` 6555 <div class="edit-modal"> 6556 <div class="edit-modal-title">Edit Post</div> 6557 <textarea id="edit-textarea">${escapeHtml(currentText)}</textarea> 6558 <div class="edit-modal-actions"> 6559 <button class="edit-modal-cancel" onclick="this.closest('.edit-overlay').remove()">Cancel</button> 6560 <button class="edit-modal-save" onclick="savePostEdit('${postId}', this)">Save</button> 6561 </div> 6562 </div>`; 6563 document.body.appendChild(overlay); 6564 overlay.querySelector('textarea').focus(); 6565} 6566 6567async function savePostEdit(postId, btn) { 6568 const overlay = btn.closest('.edit-overlay'); 6569 const newBody = overlay.querySelector('textarea').value.trim(); 6570 if (!newBody) return; 6571 6572 btn.textContent = 'Saving...'; 6573 await sbClient.from('posts').update({ body: newBody }).eq('id', postId); 6574 overlay.remove(); 6575 await loadFeed(); 6576} 6577 6578function editComment(reflectionId, postId, btn) { 6579 const card = btn.closest('.reflection-card'); 6580 const bodyEl = card.querySelector('.reflection-body'); 6581 const currentText = bodyEl.textContent; 6582 6583 const overlay = document.createElement('div'); 6584 overlay.className = 'edit-overlay'; 6585 overlay.innerHTML = ` 6586 <div class="edit-modal"> 6587 <div class="edit-modal-title">Edit Comment</div> 6588 <textarea id="edit-textarea">${escapeHtml(currentText)}</textarea> 6589 <div class="edit-modal-actions"> 6590 <button class="edit-modal-cancel" onclick="this.closest('.edit-overlay').remove()">Cancel</button> 6591 <button class="edit-modal-save" onclick="saveCommentEdit('${reflectionId}', '${postId}', this)">Save</button> 6592 </div> 6593 </div>`; 6594 document.body.appendChild(overlay); 6595 overlay.querySelector('textarea').focus(); 6596} 6597 6598async function saveCommentEdit(reflectionId, postId, btn) { 6599 const overlay = btn.closest('.edit-overlay'); 6600 const newBody = overlay.querySelector('textarea').value.trim(); 6601 if (!newBody) return; 6602 6603 btn.textContent = 'Saving...'; 6604 await sbClient.from('reflections').update({ body: newBody }).eq('id', reflectionId); 6605 overlay.remove(); 6606 await loadFeed(); 6607 setTimeout(() => { 6608 const el = document.getElementById('reflections-' + postId); 6609 if (el) el.style.display = 'block'; 6610 }, 100); 6611} 6612 6613async function deleteReflection(reflectionId, postId) { 6614 if (!sbClient) return; 6615 showConfirmModal('Remove this comment?', async () => { 6616 await sbClient.from('reflections').delete().eq('id', reflectionId); 6617 await loadFeed(); 6618 setTimeout(() => { 6619 const el = document.getElementById('reflections-' + postId); 6620 if (el) el.style.display = 'block'; 6621 }, 100); 6622 }); 6623} 6624 6625async function toggleHeart(targetId, type) { 6626 if (!sbClient || !currentUser) return; 6627 6628 const btn = event.currentTarget; 6629 const isHearted = btn.classList.contains('hearted'); 6630 6631 // Optimistic UI 6632 btn.classList.toggle('hearted'); 6633 btn.classList.add('pulse'); 6634 setTimeout(() => btn.classList.remove('pulse'), 300); 6635 6636 const icon = btn.querySelector('.heart-icon'); 6637 const count = btn.querySelector('.heart-count'); 6638 const currentCount = parseInt(count.textContent) || 0; 6639 6640 if (isHearted) { 6641 icon.innerHTML = '♡'; 6642 count.textContent = currentCount - 1 > 0 ? currentCount - 1 : ''; 6643 await sbClient.from('hearts').delete().match({ 6644 user_id: currentUser.id, 6645 [type === 'post' ? 'post_id' : 'reflection_id']: targetId 6646 }); 6647 } else { 6648 icon.innerHTML = '♥'; 6649 count.textContent = currentCount + 1; 6650 await sbClient.from('hearts').insert({ 6651 user_id: currentUser.id, 6652 [type === 'post' ? 'post_id' : 'reflection_id']: targetId 6653 }); 6654 } 6655} 6656 6657// ============================================================ 6658// TEACHINGS VIEW (filtered feed) 6659// ============================================================ 6660async function loadTeachings() { 6661 const container = document.getElementById('teachings-container'); 6662 const toolbar = document.getElementById('teachings-toolbar'); 6663 6664 if (!sbClient) { 6665 toolbar.style.display = 'none'; 6666 container.innerHTML = '<div class="placeholder-view"><div class="placeholder-icon">✦</div><div class="placeholder-title">Teachings & Transmissions</div><p class="placeholder-text" style="margin-top:6px;font-style:italic;color:var(--plum);">Yours and Mine.</p></div>'; 6667 return; 6668 } 6669 6670 const { data: posts } = await sbClient 6671 .from('posts') 6672 .select(`*, author:profiles!posts_author_id_fkey(display_name, role), hearts(user_id), post_reactions(id, user_id, emoji)`) 6673 .in('post_type', ['teaching', 'announcement']) 6674 .order('created_at', { ascending: false }); 6675 6676 if (!posts || posts.length === 0) { 6677 toolbar.style.display = 'none'; 6678 container.innerHTML = '<div class="placeholder-view"><div class="placeholder-icon">✦</div><div class="placeholder-title">Teachings & Transmissions</div><p class="placeholder-text" style="margin-top:6px;font-style:italic;color:var(--plum);">Yours and Mine.</p></div>'; 6679 return; 6680 } 6681 6682 toolbar.style.display = 'flex'; 6683 container.innerHTML = posts.map(post => { 6684 const date = new Date(post.created_at).toLocaleDateString('en-US', { month: 'long', day: 'numeric', year: 'numeric' }); 6685 const typeLabel = post.post_type === 'prompt' ? 'Journal Prompt' : 'Teaching'; 6686 return ` 6687 <div class="teachings-card post-card" data-type="${post.post_type}" style="cursor:pointer;" onclick="switchView('feed')"> 6688 <div class="post-type-label">${typeLabel}</div> 6689 ${post.title ? `<div class="post-title">${escapeHtml(post.title)}</div>` : ''} 6690 <div class="post-body" style="max-height:100px;overflow:hidden;">${escapeHtml(post.body)}</div> 6691 <div class="post-meta"> 6692 <div class="post-author">
6693 <span class="post-author-name">${escapeHtml(post.author?.display_name || 'Sumir')}</span> 6694 <span class="post-date">${date}</span> 6695 </div> 6696 </div> 6697 </div>`; 6698 }).join(''); 6699} 6700 6701// Teachings filter & search 6702let teachingsFilter = 'all'; 6703let teachingsSearch = ''; 6704 6705function filterTeachings(type) { 6706 teachingsFilter = type; 6707 document.querySelectorAll('[data-tfilter]').forEach(btn => { 6708 btn.classList.toggle('active', btn.dataset.tfilter === type); 6709 }); 6710 applyTeachingsFilters(); 6711} 6712 6713function searchTeachings(query) { 6714 teachingsSearch = query.toLowerCase().trim(); 6715 document.getElementById('teachings-search-clear').classList.toggle('show', teachingsSearch.length > 0); 6716 applyTeachingsFilters(); 6717} 6718 6719function clearTeachingsSearch() { 6720 document.getElementById('teachings-search-input').value = ''; 6721 teachingsSearch = ''; 6722 document.getElementById('teachings-search-clear').classList.remove('show'); 6723 applyTeachingsFilters(); 6724} 6725 6726function applyTeachingsFilters() { 6727 const cards = document.querySelectorAll('#teachings-container .teachings-card'); 6728 let visibleCount = 0; 6729 6730 cards.forEach(card => { 6731 const type = card.dataset.type; 6732 const title = card.querySelector('.post-title'); 6733 const body = card.querySelector('.post-body'); 6734 6735 const matchesFilter = teachingsFilter === 'all' || type === teachingsFilter; 6736 6737 let matchesSearch = true; 6738 if (teachingsSearch) { 6739 const text = [(title ? title.textContent : ''), (body ? body.textContent : '')].join(' ').toLowerCase(); 6740 matchesSearch = text.includes(teachingsSearch); 6741 } 6742 6743 const visible = matchesFilter && matchesSearch; 6744 card.style.display = visible ? '' : 'none'; 6745 if (visible) visibleCount++; 6746 }); 6747 6748 // No results message 6749 let noResults = document.getElementById('teachings-no-results'); 6750 if (visibleCount === 0 && cards.length > 0) { 6751 if (!noResults) { 6752 noResults = document.createElement('div'); 6753 noResults.id = 'teachings-no-results'; 6754 noResults.className = 'feed-empty'; 6755 noResults.innerHTML = '<div class="feed-empty-icon">✦</div><p class="feed-empty-text" style="font-style:italic;">No teachings match your search.</p>'; 6756 document.getElementById('teachings-container').appendChild(noResults); 6757 } 6758 noResults.style.display = ''; 6759 } else if (noResults) { 6760 noResults.style.display = 'none'; 6761 } 6762} 6763 6764// ============================================================ 6765// JOURNAL 6766// ============================================================ 6767async function loadJournal() { 6768 if (!sbClient || !currentUser) return; 6769 6770 // Load prompts (posts with type 'prompt') 6771 const { data: prompts } = await sbClient 6772 .from('posts') 6773 .select('*') 6774 .eq('post_type', 'prompt') 6775 .order('created_at', { ascending: false }) 6776 .limit(3); 6777 6778 const promptsContainer = document.getElementById('journal-prompts'); 6779 // Journal prompts are hidden from mentees until Sumir is ready to use them 6780 const showPrompts = currentProfile && currentProfile.role === 'teacher'; 6781 if (showPrompts && prompts && prompts.length > 0) { 6782 promptsContainer.innerHTML = prompts.map(p => ` 6783 <div class="journal-prompt-card"> 6784 <div class="journal-prompt-label">Journal Prompt</div> 6785 <div class="journal-prompt-text">${escapeHtml(p.body)}</div> 6786 </div>`).join(''); 6787 } else { 6788 promptsContainer.innerHTML = ''; 6789 } 6790 6791 // Load user's own journal entries (with reactions + replies for shared ones) 6792 const { data: ownEntries } = await sbClient 6793 .from('journal_entries') 6794 .select('*, journal_reactions(id, user_id, emoji), journal_replies(id, user_id, body, created_at, author:profiles!journal_replies_user_id_fkey(display_name))') 6795 .eq('user_id', currentUser.id) 6796 .order('created_at', { ascending: false }); 6797 6798 // If teacher, also load public entries from students 6799 let studentEntries = []; 6800 if (currentProfile && currentProfile.role === 'teacher') { 6801 const { data: pubEntries } = await sbClient 6802 .from('journal_entries') 6803 .select('*, author:profiles!journal_entries_user_id_fkey(display_name), journal_reactions(id, user_id, emoji), journal_replies(id, user_id, body, created_at, author:profiles!journal_replies_user_id_fkey(display_name))') 6804 .eq('visibility', 'public') 6805 .neq('user_id', currentUser.id) 6806 .order('created_at', { ascending: false }); 6807 studentEntries = pubEntries || []; 6808 } 6809 6810 const entriesContainer = document.getElementById('journal-entries'); 6811 const printTrigger = document.getElementById('journal-print-trigger'); 6812 const toolbar = document.getElementById('journal-toolbar'); 6813 const allEntries = ownEntries || []; 6814 6815 if (allEntries.length > 0 || studentEntries.length > 0) { 6816 if (toolbar) toolbar.style.display = 'flex'; 6817 if (printTrigger) printTrigger.style.display = 'flex'; 6818 let html = ''; 6819 6820 // Own entries (filter out inquiry responses â they show in the inquiry banner) 6821 const visibleEntries = allEntries.filter(e => !e.body.startsWith('::thoughtful-inquiries::')); 6822 if (visibleEntries.length > 0) { 6823 html += visibleEntries.map(e => { 6824 const date = new Date(e.created_at).toLocaleDateString('en-US', { weekday: 'long', month: 'long', day: 'numeric', year: 'numeric' }); 6825 const vis = e.visibility || 'private'; 6826 const isDraft = !!e.is_draft; 6827 const visLabel = isDraft ? '✎ Draft' : (vis === 'public' ? '👁 Shared with Sumir' : '🔒 Private'); 6828 const visClass = isDraft ? 'private' : (vis === 'public' ? 'public' : 'private'); 6829 const toggleVisLabel = vis === 'public' ? 'Make Private' : 'Share with Sumir'; 6830 const toggleVisValue = vis === 'public' ? 'private' : 'public'; 6831 const threadHtml = (!isDraft && vis === 'public') ? renderJournalThread(e)
6831: ''; 6832 const draftBadge = isDraft ? '<span class="journal-draft-badge">Draft</span>' : ''; 6833 const publishBtn = isDraft ? `<button class="journal-entry-action-btn vis-btn" onclick="event.stopPropagation();publishDraft('${e.id}')" style="border-color:var(--berry);color:var(--berry);">✦ Publish</button>` : `<button class="journal-entry-action-btn vis-btn" onclick="event.stopPropagation();toggleJournalVisibility('${e.id}','${toggleVisValue}')">â ${toggleVisLabel}</button>`; 6834 return ` 6835 <div class="journal-entry-card" data-entry-body="${escapeHtml(e.body).toLowerCase()}" data-entry-date="${date.toLowerCase()}" data-is-draft="${isDraft}" onclick="toggleJournalEntry(this, event)"> 6836 <div style="display:flex;align-items:center;flex-wrap:wrap;"> 6837 <div class="journal-entry-date">${date}</div> 6838 <span class="journal-entry-visibility ${visClass}">${visLabel}</span> 6839 ${draftBadge} 6840 </div> 6841 <div class="journal-entry-body truncated">${escapeHtml(e.body)}</div> 6842 <div class="journal-entry-actions"> 6843 <button class="journal-entry-action-btn edit-btn" onclick="event.stopPropagation();editJournalEntry('${e.id}','${vis}')">â Edit</button> 6844 ${publishBtn} 6845 <button class="journal-entry-action-btn delete-btn" onclick="event.stopPropagation();deleteJournalEntry('${e.id}')">â Delete</button> 6846 </div> 6847 ${threadHtml} 6848 <div class="journal-entry-collapse-hint">tap to close</div> 6849 </div>`; 6850 }).join(''); 6851 } 6852 6853 // Student entries (teacher view) 6854 if (studentEntries.length > 0) { 6855 // Separate inquiry responses from regular entries 6856 const inquiryEntries = studentEntries.filter(e => e.body.startsWith('::thoughtful-inquiries::')); 6857 const regularStudentEntries = studentEntries.filter(e => !e.body.startsWith('::thoughtful-inquiries::')); 6858 6859 // Render inquiry responses first 6860 if (inquiryEntries.length > 0) { 6861 html += '<div style="margin-top:30px;padding-top:20px;border-top:2px solid var(--gold-pale);"><div class="journal-compose-label" style="margin-bottom:16px;color:var(--berry);">Thoughtful Inquiry Responses</div>'; 6862 html += inquiryEntries.map(e => { 6863 const date = new Date(e.created_at).toLocaleDateString('en-US', { month: 'long', day: 'numeric', year: 'numeric' }); 6864 const authorName = e.author?.display_name || 'Sister'; 6865 const threadHtml = renderJournalThread(e); 6866 let formattedBody = ''; 6867 try { 6868 const responses = JSON.parse(e.body.replace('::thoughtful-inquiries::', '')); 6869 const labels = { 6870 q1: 'What are you hoping to experience?', 6871 q2: 'What are you prepared to release and transform?', 6872 q3: 'What are you prepared to offer to this space?', 6873 q4_emotional: 'Emotional', q4_energetic: 'Energetic', q4_spiritual: 'Spiritual', 6874 q4_practical: 'Practical', q4_physical: 'Physical', q4_facilitator: 'Facilitator Experience', 6875 q5_excited: 'Most excited about', q5_fearful: 'Most fearful/nervous about' 6876 }; 6877 // Group responses 6878 const groups = {}; 6879 Object.entries(responses).forEach(([key, val]) => { 6880 const prefix = key.replace(/_\d+$/, ''); 6881 if (!groups[prefix]) groups[prefix] = []; 6882 groups[prefix].push(val); 6883 }); 6884 Object.entries(groups).forEach(([prefix, vals]) => { 6885 const label = labels[prefix] || prefix; 6886 formattedBody += '<div style="margin-bottom:12px;"><span style="font-family:Marcellus,serif;font-size:10px;letter-spacing:1.5px;text-transform:uppercase;color:var(--berry);">' + escapeHtml(label) + '</span>'; 6887 vals.forEach(v => { formattedBody += '<div style="padding-left:12px;font-size:13px;line-height:1.7;opacity:0.88;">♦︎ ' + escapeHtml(v) + '</div>'; }); 6888 formattedBody += '</div>'; 6889 }); 6890 } catch (err) { formattedBody = escapeHtml(e.body); } 6891 return ` 6892 <div class="journal-entry-card" style="border-left:3px solid var(--berry);" onclick="toggleJournalEntry(this, event)"> 6893 <div style="display:flex;align-items:center;justify-content:space-between;flex-wrap:wrap;"> 6894 <div class="journal-entry-date">${escapeHtml(authorName)} — ${date}</div>
6895 <span style="font-family:'Marcellus',serif;font-size:8px;letter-spacing:1.5px;padding:2px 7px;border-radius:8px;background:rgba(148,14,75,0.1);color:var(--berry);">INQUIRY</span> 6896 </div> 6897 <div class="journal-entry-body truncated">${formattedBody}</div> 6898 ${threadHtml} 6899 <div class="journal-entry-collapse-hint">tap to close</div> 6900 </div>`; 6901 }).join(''); 6902 html += '</div>'; 6903 } 6904 6905 // Render regular shared entries 6906 if (regularStudentEntries.length > 0) { 6907 html += '<div style="margin-top:30px;padding-top:20px;border-top:2px solid var(--gold-pale);"><div class="journal-compose-label" style="margin-bottom:16px;color:var(--berry);">Shared With You</div>'; 6908 html += regularStudentEntries.map(e => { 6909 const date = new Date(e.created_at).toLocaleDateString('en-US', { weekday: 'long', month: 'long', day: 'numeric', year: 'numeric' }); 6910 const authorName = e.author?.display_name || 'Sister'; 6911 const threadHtml = renderJournalThread(e); 6912 return ` 6913 <div class="journal-entry-card" style="border-left:3px solid var(--berry);" onclick="toggleJournalEntry(this, event)"> 6914 <div style="display:flex;align-items:center;justify-content:space-between;flex-wrap:wrap;"> 6915 <div class="journal-entry-date">${date}</div> 6916 <span style="font-family:'Marcellus',serif;font-size:9px;letter-spacing:1px;color:var(--berry);">${escapeHtml(authorName)}</span> 6917 </div> 6918 <div class="journal-entry-body truncated">${escapeHtml(e.body)}</div> 6919 ${threadHtml} 6920 <div class="journal-entry-collapse-hint">tap to close</div> 6921 </div>`; 6922 }).join(''); 6923 html += '</div>'; 6924 } 6925 } 6926 6927 entriesContainer.innerHTML = html; 6928 } else { 6929 if (toolbar) toolbar.style.display = 'none'; 6930 if (printTrigger) printTrigger.style.display = 'none'; 6931 entriesContainer.innerHTML = ''; 6932 } 6933} 6934 6935// Search across journal entries (own entries only, by date and body text) 6936function filterJournalEntries(query) { 6937 const q = (query || '').trim().toLowerCase(); 6938 const clearBtn = document.getElementById('journal-search-clear'); 6939 if (clearBtn) clearBtn.style.display = q ? 'block' : 'none'; 6940 6941 const cards = document.querySelectorAll('#journal-entries .journal-entry-card'); 6942 let visible = 0; 6943 cards.forEach(card => { 6944 if (!q) { 6945 card.style.display = ''; 6946 visible++; 6947 return; 6948 } 6949 const body = card.dataset.entryBody || ''; 6950 const date = card.dataset.entryDate || ''; 6951 const match = body.indexOf(q) !== -1 || date.indexOf(q) !== -1; 6952 card.style.display = match ? '' : 'none'; 6953 if (match) visible++; 6954 }); 6955 6956 // Show "no results" state 6957 let noResults = document.getElementById('journal-no-results'); 6958 if (q && visible === 0) { 6959 if (!noResults) { 6960 noResults = document.createElement('div'); 6961 noResults.id = 'journal-no-results'; 6962 noResults.style.cssText = 'text-align:center;padding:40px 20px;color:var(--plum);font-family:Cormorant Garamond,serif;font-style:italic;font-size:18px;'; 6963 noResults.innerHTML = '<div style="font-size:24px;color:var(--gold);margin-bottom:8px;">✦</div>No entries match your search.'; 6964 document.getElementById('journal-entries').appendChild(noResults); 6965 } 6966 noResults.style.display = ''; 6967 } else if (noResults) { 6968 noResults.style.display = 'none'; 6969 } 6970} 6971 6972function clearJournalSearch() { 6973 const input = document.getElementById('journal-search-input'); 6974 if (input) input.value = ''; 6975 filterJournalEntries(''); 6976} 6977 6978let journalVisibility = 'private'; 6979 6980function setJournalVisibility(vis) { 6981 journalVisibility = vis; 6982 document.getElementById('vis-private').classList.toggle('active', vis === 'private'); 6983 document.getElementById('vis-public').classList.toggle('active', vis === 'public'); 6984} 6985 6986// Track which draft is currently being edited (if any) 6987let editingDraftId = null; 6988 6989async function saveJournalEntry(asDraft) { 6990 if (!sbClient || !currentUser) return; 6991 6992 const body = document.getElementById('journal-body').value.trim(); 6993 if (!body) return; 6994 6995 const btn = asDraft 6996 ? document.querySelector('.journal-save-draft') 6997 : document.querySelector('.journal-save:not(.journal-save-draft)'); 6998 setButtonLoading(btn, true, asDraft ? 'Saving draft...' : 'Saving...'); 6999 7000 try { 7001 let error; 7002 if (editingDraftId) { 7003 // Updating an existing draft 7004 const updateData = { 7005 body, 7006 visibility: journalVisibility, 7007 is_draft: !!asDraft 7008 }; 7009 ({ error } = await sbClient.from('journal_entries').update(updateData).eq('id', editingDraftId)); 7010 } else { 7011 ({ error } = await sbClient.from('journal_entries').insert({ 7012 user_id: currentUser.id, 7013 body, 7014 visibility: journalVisibility, 7015 is_draft: !!asDraft 7016 })); 7017 } 7018 if (error) throw error; 7019 7020 document.getElementById('journal-body').value = ''; 7021 journalVisibility = 'private'; 7022 setJournalVisibility('private'); 7023 editingDraftId = null; 7024 document.getElementById('editing-draft-banner').style.display = 'none'; 7025 await loadJournal(); 7026 showToast(asDraft ? 'Draft saved' : 'Entry saved', 'success'); 7027 } catch (e) { 7028 showToast('Could not save: ' + (e.message || e), 'error'); 7029 } finally { 7030 setButtonLoading(btn, false); 7031 } 7032} 7033 7034// ============================================================ 7035// THOUGHTFUL INQUIRIES â Save & Load 7036// ============================================================ 7037const INQUIRY_JOURNAL_TAG = '::thoughtful-inquiries::'; 7038 7039async function loadInquiryResponses() { 7040 if (!sbClient || !currentUser) return; 7041 7042 // Look for an existing inquiry journal entry 7043 const { data: entries } = await sbClient 7044 .from('journal_entries') 7045 .select('id, body') 7046 .eq('user_id', currentUser.id) 7047 .like('body', INQUIRY_JOURNAL_TAG + '%') 7048 .limit(1); 7049
7050 if (entries && entries.length > 0) { 7051 try { 7052 const jsonStr = entries[0].body.replace(INQUIRY_JOURNAL_TAG, ''); 7053 const responses = JSON.parse(jsonStr); 7054 // Populate inputs 7055 document.querySelectorAll('.inquiry-input').forEach(input => { 7056 const field = input.dataset.field; 7057 if (field && responses[field]) { 7058 input.value = responses[field]; 7059 } 7060 }); 7061 // Store the entry ID for updates 7062 document.getElementById('inquiry-banner').dataset.entryId = entries[0].id; 7063 } catch (e) { /* ignore parse errors */ } 7064 } 7065} 7066 7067async function saveInquiryResponses() { 7068 if (!sbClient || !currentUser) return; 7069 7070 const btn = document.querySelector('.inquiry-save-btn'); 7071 const msg = document.getElementById('inquiry-saved-msg'); 7072 btn.textContent = 'SAVING...'; 7073 btn.disabled = true; 7074 7075 // Gather all responses 7076 const responses = {}; 7077 document.querySelectorAll('.inquiry-input').forEach(input => { 7078 const field = input.dataset.field; 7079 if (field && input.value.trim()) { 7080 responses[field] = input.value.trim(); 7081 } 7082 }); 7083 7084 const body = INQUIRY_JOURNAL_TAG + JSON.stringify(responses); 7085 const banner = document.getElementById('inquiry-banner'); 7086 const existingId = banner.dataset.entryId; 7087 7088 try { 7089 if (existingId) { 7090 // Update existing entry â always keep inquiry visible to Sumir 7091 const { error } = await sbClient.from('journal_entries') 7092 .update({ body, visibility: 'public' }) 7093 .eq('id', existingId); 7094 if (error) throw error; 7095 } else { 7096 // Create new entry â inquiry responses are always visible to Sumir 7097 const { data, error } = await sbClient.from('journal_entries') 7098 .insert({ user_id: currentUser.id, body, visibility: 'public', is_draft: false }) 7099 .select('id') 7100 .single(); 7101 if (error) throw error; 7102 if (data) banner.dataset.entryId = data.id; 7103 } 7104 7105 msg.classList.add('show'); 7106 setTimeout(() => msg.classList.remove('show'), 3000); 7107 showToast('Inquiry responses saved as Journal Entry #1', 'success'); 7108 } catch (e) { 7109 showToast('Could not save: ' + (e.message || e), 'error'); 7110 } finally { 7111 btn.textContent = 'SAVE MY RESPONSES'; 7112 btn.disabled = false; 7113 } 7114} 7115 7116async function publishDraft(entryId) { 7117 if (!sbClient || !currentUser) return; 7118 showConfirmModal('Publish this draft as a journal entry?', async () => { 7119 const { error } = await sbClient.from('journal_entries').update({ is_draft: false }).eq('id', entryId); 7120 if (error) { 7121 showToast('Could not publish: ' + error.message, 'error'); 7122 return; 7123 } 7124 showToast('Draft published', 'success'); 7125 await loadJournal(); 7126 }); 7127} 7128 7129function cancelDraftEdit() { 7130 editingDraftId = null; 7131 document.getElementById('journal-body').value = ''; 7132 document.getElementById('editing-draft-banner').style.display = 'none'; 7133 setJournalVisibility('private'); 7134} 7135 7136function toggleJournalEntry(card, evt) { 7137 // Don't collapse when clicking inside the thread (reactions, reply input, etc.) 7138 if (evt && evt.target.closest('.journal-thread')) return; 7139 if (card.classList.contains('expanded')) { 7140 card.classList.remove('expanded'); 7141 } else { 7142 // Collapse any other expanded entries 7143 document.querySelectorAll('.journal-entry-card.expanded').forEach(c => c.classList.remove('expanded')); 7144 card.classList.add('expanded'); 7145 } 7146} 7147 7148// ============================================================ 7149// JOURNAL REACTIONS + REPLIES (private 1-on-1 thread with Sumir) 7150// ============================================================ 7151const JOURNAL_EMOJIS = ['â¤ï¸', 'â¨', 'ð', 'ð', 'ð¤']; 7152 7153function renderJournalThread(entry) { 7154 const reactions = entry.journal_reactions || []; 7155 const replies = (entry.journal_replies || []).slice().sort((a, b) => new Date(a.created_at) - new Date(b.created_at)); 7156 7157 // Group reactions by emoji 7158 const grouped = {}; 7159 reactions.forEach(r => { 7160 if (!grouped[r.emoji]) grouped[r.emoji] = []; 7161 grouped[r.emoji].push(r); 7162 }); 7163 7164 let chipsHtml = Object.keys(grouped).map(emoji => { 7165 const list = grouped[emoji]; 7166 const mine = currentUser && list.some(r => r.user_id === currentUser.id); 7167 return `<button class="j-reaction-chip ${mine ? 'mine' : ''}" onclick="event.stopPropagation();toggleJournalReaction('${entry.id}','${emoji}')" aria-label="${list.length} ${emoji} reaction${list.length === 1 ? '' : 's'}${mine ? ', tap to remove yours' : ', tap to add yours'}" aria-pressed="${mine}"><span aria-hidden="true">${emoji}</span><span class="j-reaction-count" aria-hidden="true">${list.length}</span></button>`; 7168 }).join(''); 7169 7170 const pickerHtml = ` 7171 <span class="j-emoji-picker" onclick="event.stopPropagation();"> 7172 ${JOURNAL_EMOJIS.map(em => `<button class="j-emoji-btn" onclick="event.stopPropagation();toggleJournalReaction('${entry.id}','${em}')" aria-label="React with ${em}"><span aria-hidden="true">${em}</span></button>`).join('')} 7173 </span>`;
7174 7175 const repliesHtml = replies.map(r => { 7176 const isMine = currentUser && r.user_id === currentUser.id; 7177 const author = r.author?.display_name || 'Sister'; 7178 const dateStr = new Date(r.created_at).toLocaleDateString('en-US', { month: 'short', day: 'numeric' }) + ' · ' + new Date(r.created_at).toLocaleTimeString('en-US', { hour: 'numeric', minute: '2-digit' }); 7179 return ` 7180 <div class="j-reply"> 7181 ${isMine ? `<button class="j-reply-delete" onclick="event.stopPropagation();deleteJournalReply('${r.id}')">â</button>` : ''} 7182 <div class="j-reply-author">${escapeHtml(author)}</div> 7183 <div class="j-reply-body">${escapeHtml(r.body)}</div> 7184 <div class="j-reply-date">${dateStr}</div> 7185 </div>`; 7186 }).join(''); 7187 7188 return ` 7189 <div class="journal-thread" onclick="event.stopPropagation();"> 7190 <div class="j-reactions-row"> 7191 ${chipsHtml} 7192 ${pickerHtml} 7193 </div> 7194 <div class="j-replies-list">${repliesHtml}</div> 7195 <div class="j-reply-form"> 7196 <input type="text" class="j-reply-input" id="j-reply-input-${entry.id}" placeholder="Write a replyâ¦" onclick="event.stopPropagation();" onkeydown="if(event.key==='Enter'){event.preventDefault();sendJournalReply('${entry.id}');}"> 7197 <button class="j-reply-send" onclick="event.stopPropagation();sendJournalReply('${entry.id}')">Send</button> 7198 </div> 7199 </div>`; 7200} 7201 7202async function toggleJournalReaction(entryId, emoji) { 7203 if (!sbClient || !currentUser) return; 7204 // Check if this user already reacted with this emoji 7205 const { data: existing } = await sbClient 7206 .from('journal_reactions') 7207 .select('id') 7208 .eq('journal_entry_id', entryId) 7209 .eq('user_id', currentUser.id) 7210 .eq('emoji', emoji); 7211 7212 if (existing && existing.length > 0) { 7213 await sbClient.from('journal_reactions').delete().eq('id', existing[0].id); 7214 } else { 7215 await sbClient.from('journal_reactions').insert({ 7216 journal_entry_id: entryId, 7217 user_id: currentUser.id, 7218 emoji 7219 }); 7220 } 7221 await loadJournal(); 7222 // Re-expand the entry that was open 7223 const card = document.querySelector(`[onclick*="${entryId}"]`)?.closest('.journal-entry-card'); 7224 if (card) card.classList.add('expanded'); 7225} 7226 7227async function sendJournalReply(entryId) { 7228 if (!sbClient || !currentUser) return; 7229 const input = document.getElementById('j-reply-input-' + entryId); 7230 if (!input) return; 7231 const body = input.value.trim(); 7232 if (!body) return; 7233 7234 const btn = input.parentElement?.querySelector('.j-reply-send'); 7235 setButtonLoading(btn, true, '...'); 7236 7237 try { 7238 const { error } = await sbClient.from('journal_replies').insert({ 7239 journal_entry_id: entryId, 7240 user_id: currentUser.id, 7241 body 7242 }); 7243 if (error) throw error; 7244 input.value = ''; 7245 showToast('Reply sent', 'success'); 7246 await loadJournal(); 7247 // Re-expand the entry that was open 7248 const card = document.querySelector(`[onclick*="${entryId}"]`)?.closest('.journal-entry-card'); 7249 if (card) card.classList.add('expanded'); 7250 } catch (e) { 7251 showToast('Could not send reply: ' + (e.message || e), 'error'); 7252 } finally { 7253 setButtonLoading(btn, false); 7254 } 7255} 7256 7257async function deleteJournalReply(replyId) { 7258 if (!sbClient || !currentUser) return; 7259 showConfirmModal('Delete this reply?', async () => { 7260 await sbClient.from('journal_replies').delete().eq('id', replyId); 7261 await loadJournal(); 7262 }); 7263} 7264 7265function editJournalEntry(entryId, currentVis) { 7266 if (!sbClient || !currentUser) return; 7267 // Fetch the entry to get current body 7268 sbClient.from('journal_entries').select('*').eq('id', entryId).single().then(({ data: entry }) => { 7269 if (!entry) return; 7270 7271 // If it's a draft, load it back into the main composer for continued editing 7272 if (entry.is_draft) { 7273 editingDraftId = entry.id; 7274 document.getElementById('journal-body').value = entry.body; 7275 journalVisibility = entry.visibility || 'private'; 7276 setJournalVisibility(journalVisibility); 7277 document.getElementById('editing-draft-banner').style.display = 'block'; 7278 document.getElementById('journal-body').focus(); 7279 // Scroll the composer into view 7280 document.querySelector('.journal-compose').scrollIntoView({ behavior: 'smooth', block: 'center' }); 7281 return; 7282 } 7283 7284 // For published entries, use the inline edit modal
7285 const overlay = document.createElement('div'); 7286 overlay.className = 'edit-overlay'; 7287 overlay.innerHTML = ` 7288 <div class="edit-modal"> 7289 <div class="edit-modal-title">Edit Journal Entry</div> 7290 <textarea class="edit-modal-textarea" id="edit-journal-textarea" style="min-height:160px;">${escapeHtml(entry.body)}</textarea> 7291 <div class="edit-modal-actions"> 7292 <button class="edit-modal-cancel" onclick="this.closest('.edit-overlay').remove()">Cancel</button> 7293 <button class="edit-modal-save" onclick="saveJournalEdit('${entryId}')">Save</button> 7294 </div> 7295 </div>`; 7296 document.body.appendChild(overlay); 7297 setTimeout(() => document.getElementById('edit-journal-textarea').focus(), 100); 7298 }); 7299} 7300 7301async function saveJournalEdit(entryId) { 7302 const textarea = document.getElementById('edit-journal-textarea'); 7303 const newBody = textarea.value.trim(); 7304 if (!newBody) return; 7305 await sbClient.from('journal_entries').update({ body: newBody }).eq('id', entryId); 7306 document.querySelector('.edit-overlay').remove(); 7307 await loadJournal(); 7308} 7309 7310function deleteJournalEntry(entryId) { 7311 showConfirmModal('Are you sure you want to delete this journal entry?', async () => { 7312 await sbClient.from('journal_entries').delete().eq('id', entryId); 7313 await loadJournal(); 7314 }); 7315} 7316 7317async function toggleJournalVisibility(entryId, newVisibility) { 7318 await sbClient.from('journal_entries').update({ visibility: newVisibility }).eq('id', entryId); 7319 await loadJournal(); 7320} 7321 7322function printJournal() { 7323 const entries = document.querySelectorAll('#journal-entries .journal-entry-card'); 7324 if (entries.length === 0) return; 7325 7326 const displayName = document.getElementById('sidebar-user-name-text').textContent || 'Sister'; 7327 document.getElementById('journal-print-name').textContent = displayName; 7328 7329 // Build print entries 7330 const printContainer = document.getElementById('journal-print-entries'); 7331 printContainer.innerHTML = ''; 7332 entries.forEach(entry => { 7333 const date = entry.querySelector('.journal-entry-date').textContent; 7334 const body = entry.querySelector('.journal-entry-body').textContent; 7335 printContainer.innerHTML += ` 7336 <div class="journal-print-entry"> 7337 <div class="journal-print-date">${date}</div> 7338 <div class="journal-print-body">${escapeHtml(body)}</div> 7339 </div>`; 7340 }); 7341 7342 // Show print area, print, then hide 7343 const printArea = document.getElementById('journal-print-area'); 7344 printArea.style.display = 'block'; 7345 window.print(); 7346 printArea.style.display = 'none'; 7347} 7348 7349// ============================================================ 7350// ============================================================ 7351// REPLAYS 7352// ============================================================ 7353let pendingReplayFile = null; 7354 7355function handleReplayVideoSelect(input) { 7356 const file = input.files[0]; 7357 if (!file) return; 7358 pendingReplayFile = file; 7359 document.getElementById('replay-video-name').textContent = file.name; 7360} 7361 7362async function uploadReplay() { 7363 if (!sbClient || !currentUser) return; 7364 const title = document.getElementById('replay-title').value.trim(); 7365 if (!title) { showToast('Please enter a session title.', 'error'); return; } 7366 if (!pendingReplayFile) { showToast('Please choose a video file.', 'error'); return; } 7367 7368 const btn = document.getElementById('replay-upload-btn'); 7369 btn.disabled = true; 7370 btn.textContent = 'Uploading...'; 7371 7372 try { 7373 const result = await uploadMedia(pendingReplayFile); 7374 if (!result) { 7375 btn.disabled = false; 7376 btn.textContent = 'Upload Replay'; 7377 return; 7378 } 7379 7380 const desc = document.getElementById('replay-desc').value.trim(); 7381 const { error } = await sbClient.from('replays').insert({ 7382 title, 7383 description: desc || null, 7384 video_url: result.url, 7385 uploaded_by: currentUser.id 7386 }); 7387 7388 if (error) { 7389 console.error('Replay insert error:', error); 7390 showToast('Could not save replay: ' + error.message, 'error'); 7391 } else { 7392 document.getElementById('replay-title').value = ''; 7393 document.getElementById('replay-desc').value = ''; 7394 document.getElementById('replay-video-name').textContent = ''; 7395 document.getElementById('replay-video-input').value = ''; 7396 pendingReplayFile = null; 7397 await loadReplays(); 7398 // Auto-announce in community 7399 await sbClient.from('posts').insert({ 7400 author_id: currentUser.id, 7401 title: '▶ Your Replay is Up!', 7402 body: title + ' is now available in the Replays section.', 7403 post_type: 'announcement' 7404 }); 7405 } 7406 } catch (e) { 7407 console.error('Replay exception:', e); 7408 showToast('Something went wrong: ' + e.message, 'error'); 7409 } 7410 7411 btn.disabled = false;
7412 btn.textContent = 'Upload Replay'; 7413} 7414 7415async function loadReplays() { 7416 if (!sbClient) return; 7417 7418 // Show upload form for teachers 7419 if (currentProfile && currentProfile.role === 'teacher') { 7420 document.getElementById('replay-upload').style.display = 'block'; 7421 } 7422 7423 const { data: replays, error } = await sbClient 7424 .from('replays') 7425 .select('*') 7426 .order('created_at', { ascending: false }); 7427 7428 const container = document.getElementById('replays-container'); 7429 const emptyState = document.getElementById('replays-empty'); 7430 7431 if (!replays || replays.length === 0) { 7432 container.innerHTML = ''; 7433 container.appendChild(emptyState); 7434 emptyState.style.display = 'block'; 7435 return; 7436 } 7437 7438 const isTeacher = currentProfile && currentProfile.role === 'teacher'; 7439 7440 container.innerHTML = replays.map(r => { 7441 const date = new Date(r.created_at).toLocaleDateString('en-US', { weekday: 'long', month: 'long', day: 'numeric', year: 'numeric' }); 7442 return ` 7443 <div class="replay-card"> 7444 <div class="replay-card-date">${date}</div> 7445 <div class="replay-card-title">${escapeHtml(r.title)}</div> 7446 ${r.description ? `<div class="replay-card-desc">${escapeHtml(r.description)}</div>` : ''} 7447 <video controls controlslist="nodownload nofullscreen noremoteplayback" disablepictureinpicture oncontextmenu="return false;" preload="metadata" src="${r.video_url}#t=0.001"></video> 7448 ${isTeacher ? `<div class="replay-card-actions"><button class="replay-delete-btn" onclick="deleteReplay('${r.id}')">✕ Remove</button></div>` : ''} 7449 </div>`; 7450 }).join(''); 7451} 7452 7453async function deleteReplay(replayId) { 7454 showConfirmModal('Are you sure you want to remove this replay?', async () => { 7455 await sbClient.from('replays').delete().eq('id', replayId); 7456 await loadReplays(); 7457 }); 7458} 7459 7460// REALTIME 7461// ============================================================ 7462function setupRealtime() { 7463 if (!sbClient) return; 7464 7465 sbClient.channel('mentorship-feed') 7466 .on('postgres_changes', { event: 'INSERT', schema: 'public', table: 'posts' }, () => loadFeed()) 7467 .on('postgres_changes', { event: 'DELETE', schema: 'public', table: 'posts' }, () => loadFeed()) 7468 .on('postgres_changes', { event: 'INSERT', schema: 'public', table: 'reflections' }, () => loadFeed()) 7469 .on('postgres_changes', { event: 'DELETE', schema: 'public', table: 'reflections' }, () => loadFeed()) 7470 .on('postgres_changes', { event: '*', schema: 'public', table: 'hearts' }, () => { 7471 // Don't reload entire feed for hearts â handled optimistically 7472 }) 7473 .subscribe(); 7474} 7475 7476// ============================================================ 7477// UTILITIES 7478// ============================================================ 7479function escapeHtml(text) { 7480 if (!text) return ''; 7481 const div = document.createElement('div'); 7482 div.textContent = text; 7483 return div.innerHTML; 7484} 7485 7486// ============================================================ 7487// INIT 7488// ============================================================ 7489// First, try to restore an existing Supabase session (skip password + auth) 7490(async function init() { 7491 // CAPTURE URL state IMMEDIATELY â before the Supabase SDK clears it. 7492 const urlHash = window.location.hash || ''; 7493 const urlSearch = window.location.search || ''; 7494 const isRecoveryUrl = 7495 urlHash.indexOf('type=recovery') !== -1 || 7496 urlSearch.indexOf('type=recovery') !== -1 || 7497 (urlSearch.indexOf('code=') !== -1 && urlSearch.indexOf('error') === -1); // PKCE flow 7498 7499 // If this is a password-recovery flow, bypass the gate and force the reset screen. 7500 if (isRecoveryUrl) { 7501 localStorage.setItem('mentorship_authenticated', 'true'); // bypass the gate for this user 7502 document.getElementById('password-screen').style.display = 'none'; 7503 7504 if (initSupabase()) { 7505 // Listen for PASSWORD_RECOVERY (legacy/hash flow) and SIGNED_IN (PKCE flow during recovery) 7506 sbClient.auth.onAuthStateChange((event, session) => { 7507 if (event === 'PASSWORD_RECOVERY') { 7508 showResetScreen(); 7509 } else if (event === 'SIGNED_IN' && session) { 7510 // We KNOW this is a recovery flow because the URL said so. 7511 showResetScreen(); 7512 } 7513 }); 7514 7515 // For the PKCE flow, manually exchange the code if Supabase didn't auto-process it. 7516 if (urlSearch.indexOf('code=') !== -1) { 7517 try { 7518 const params = new URLSearchParams(urlSearch); 7519 const code = params.get('code'); 7520 if (code) await sbClient.auth.exchangeCodeForSession(code); 7521 } catch (e) { console.warn('Code exchange:', e); } 7522 } 7523 7524 // Safety net: force the reset screen to appear even if no auth event fires. 7525 setTimeout(() => { 7526 const reset = document.getElementById('reset-screen'); 7527 if (reset && reset.style.display !== 'flex') showResetScreen(); 7528 }, 600); 7529 return; 7530 } 7531 } 7532 7533 if (initSupabase()) { 7534 const { data: { session } } = await sbClient.auth.getSession(); 7535 if (session) { 7536 // Already logged in â skip password and auth screens entirely 7537 localStorage.setItem('mentorship_authenticated', 'true'); 7538 document.getElementById('password-screen').style.display = 'none'; 7539 handleSignedIn(session.user); 7540 7541 // Listen for auth changes 7542 sbClient.auth.onAuthStateChange((event, sess) => { 7543 if (event === 'PASSWORD_RECOVERY') { 7544 showResetScreen(); 7545 } else if (event === 'SIGNED_IN' && sess) { 7546 handleSignedIn(sess.user); 7547 } 7548 }); 7549 return; 7550 } 7551 7552 // No session yet â still wire up listeners in case PASSWORD_RECOVERY fires shortly 7553 sbClient.auth.onAuthStateChange((event, sess) => { 7554 if (event === 'PASSWORD_RECOVERY') { 7555 document.getElementById('password-screen').style.display = 'none'; 7556 showResetScreen(); 7557 } else if (event === 'SIGNED_IN' && sess) { 7558 handleSignedIn(sess.user); 7559 } 7560 }); 7561 } 7562 7563 // No active session â check password gate 7564 if (localStorage.getItem('mentorship_authenticated') === 'true') { 7565 proceedAfterPassword(); 7566 } 7567})(); 7568 7569// Handle hash routing 7570window.addEventListener('hashchange', () => { 7571 const hash = window.location.hash.replace('#', ''); 7572 if (hash && document.getElementById('hub').style.display === 'block') { 7573 switchView(hash); 7574 } 7575}); 7576 7577// ============================================================ 7578// MONTHLY HARVEST 7579// ============================================================ 7580const HARVEST_MONTHS = [ 7581 'June 2026', 'July 2026', 'August 2026', 'September 2026', 7582 'October 2026', 'November 2026', 'December 2026', 7583 'January 2027' 7584]; 7585 7586async function loadHarvests() { 7587 if (!sbClient) return; 7588 7589 const { data: harvests } = await sbClient 7590 .from('monthly_harvests') 7591 .select('*, responses:harvest_responses(*, hearts, hearted_by)') 7592 .order('created_at', { ascending: false }); 7593 7594 const container = document.getElementById('harvest-months-container'); 7595 const isTeacher = currentProfile && currentProfile.role === 'teacher'; 7596 7597 if (!harvests || harvests.length === 0) { 7598 container.innerHTML = ` 7599 <div style="text-align:center;padding:50px 20px;"> 7600 <div style="font-size:40px;margin-bottom:16px;opacity:0.3;">✦</div> 7601 <div style="font-family:'Cormorant Garamond',serif;font-size:20px;color:var(--plum-deep);margin-bottom:8px;">No harvests yet</div> 7602 <div style="font-size:13px;color:var(--charcoal);opacity:0.5;">${isTeacher ? 'Create the first monthly harvest above.' : 'Your teacher will share the first harvest prompt soon.'}</div> 7603 </div>`; 7604 return; 7605 } 7606 7607 // Group by month 7608 const byMonth = {};
7609 harvests.forEach(h => { 7610 if (!byMonth[h.month_label]) byMonth[h.month_label] = []; 7611 byMonth[h.month_label].push(h); 7612 }); 7613 7614 // Sort months by HARVEST_MONTHS order 7615 const sortedMonths = Object.keys(byMonth).sort((a, b) => { 7616 return HARVEST_MONTHS.indexOf(b) - HARVEST_MONTHS.indexOf(a); 7617 }); 7618 7619 container.innerHTML = sortedMonths.map(month => { 7620 const monthHarvests = byMonth[month]; 7621 return ` 7622 <div class="harvest-month-card" style="margin-bottom:20px;"> 7623 <div onclick="this.parentElement.classList.toggle('open')" style="display:flex;align-items:center;justify-content:space-between;padding:20px 24px;background:white;border-radius:14px;border:1px solid rgba(197,165,90,0.25);cursor:pointer;transition:all 0.3s;"> 7624 <div> 7625 <div style="font-family:'Marcellus',serif;font-size:10px;letter-spacing:3px;color:var(--gold);text-transform:uppercase;margin-bottom:4px;">${month}</div> 7626 <div style="font-family:'Cormorant Garamond',serif;font-size:18px;color:var(--plum-deep);">${monthHarvests.length} ${monthHarvests.length === 1 ? 'harvest' : 'harvests'}</div> 7627 </div> 7628 <div style="color:var(--gold);font-size:18px;transition:transform 0.3s;" class="harvest-chevron">▾</div> 7629 </div> 7630 <div class="harvest-month-content" style="display:none;padding:0 8px;"> 7631 ${monthHarvests.map(h => renderHarvestCard(h, isTeacher)).join('')} 7632 </div> 7633 </div>`; 7634 }).join(''); 7635 7636 // Auto-open the most recent month 7637 const firstCard = container.querySelector('.harvest-month-card'); 7638 if (firstCard) firstCard.classList.add('open'); 7639 7640 // Apply open state 7641 container.querySelectorAll('.harvest-month-card').forEach(card => { 7642 if (card.classList.contains('open')) { 7643 card.querySelector('.harvest-month-content').style.display = 'block'; 7644 card.querySelector('.harvest-chevron').style.transform = 'rotate(180deg)'; 7645 } 7646 }); 7647 7648 // Observe toggle 7649 const observer = new MutationObserver(mutations => { 7650 mutations.forEach(m => { 7651 if (m.attributeName === 'class') { 7652 const card = m.target; 7653 const content = card.querySelector('.harvest-month-content'); 7654 const chevron = card.querySelector('.harvest-chevron'); 7655 if (card.classList.contains('open')) { 7656 content.style.display = 'block'; 7657 chevron.style.transform = 'rotate(180deg)'; 7658 } else { 7659 content.style.display = 'none'; 7660 chevron.style.transform = 'rotate(0deg)'; 7661 } 7662 } 7663 }); 7664 }); 7665 container.querySelectorAll('.harvest-month-card').forEach(card => { 7666 observer.observe(card, { attributes: true, attributeFilter: ['class'] }); 7667 }); 7668} 7669 7670function renderHarvestCard(harvest, isTeacher) { 7671 const myResponse = harvest.responses ? harvest.responses.find(r => r.user_id === currentUser.id) : null; 7672 const responses = harvest.responses || []; 7673 const responseCount = responses.length; 7674 7675 const deleteBtn = isTeacher ? `<button onclick="deleteHarvest('${harvest.id}')" style="background:none;border:none;color:var(--charcoal);opacity:0.3;cursor:pointer;font-size:11px;padding:4px 8px;transition:opacity 0.2s;" onmouseover="this.style.opacity='0.7'" onmouseout="this.style.opacity='0.3'">✕ Remove</button>` : ''; 7676 7677 // All responses visible to everyone with hearts 7678 const allResponses = responseCount > 0 ? ` 7679 <div style="margin-top:16px;padding-top:16px;border-top:1px solid rgba(197,165,90,0.15);"> 7680 <div style="font-family:'Marcellus',serif;font-size:9px;letter-spacing:2px;color:var(--gold);text-transform:uppercase;margin-bottom:10px;">${responseCount} ${responseCount === 1 ? 'Harvest' : 'Harvests'} Shared</div> 7681 ${responses.map(r => { 7682 const heartCount = r.hearts || 0; 7683 const iHearted = r.hearted_by && r.hearted_by.includes(currentUser.id); 7684 const heartStyle = iHearted ? 'color:#C04070;opacity:1;' : 'color:var(--charcoal);opacity:0.35;'; 7685 const isOwn = r.user_id === currentUser.id; 7686 return ` 7687 <div style="background:${isOwn ? 'var(--cream-warm)' : 'var(--cream)'};border-radius:10px;padding:14px 16px;margin-bottom:8px;${isOwn ? 'border-left:3px solid var(--gold);' : ''}"> 7688 <div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:6px;"> 7689 <div style="font-size:11px;font-weight:600;color:var(--plum-deep);">${escapeHtml(r.display_name || 'Member')}${isOwn ? ' <span style="font-weight:400;opacity:0.5;">(you)</span>' : ''}</div> 7690 <div style="font-size:10px;color:var(--charcoal);opacity:0.4;">${new Date(r.created_at).toLocaleDateString('en-US', { month: 'short', day: 'numeric' })}</div> 7691 </div> 7692 <div style="font-size:13px;line-height:1.7;white-space:pre-wrap;">${escapeHtml(r.body)}</div> 7693 <div style="margin-top:8px;"> 7694 <button onclick="toggleHarvestHeart('${r.id}')" style="background:none;border:none;cursor:pointer;font-size:14px;padding:2px 6px;transition:all 0.2s;${heartStyle}" aria-label="${iHearted ? 'Remove heart' : 'Send heart'}" aria-pressed="${iHearted}">
7695 <span aria-hidden="true">${iHearted ? '❤' : '♡'}${heartCount > 0 ? ' <span style="font-size:11px;">' + heartCount + '</span>' : ''}</span> 7696 </button> 7697 </div> 7698 </div>`; 7699 }).join('')} 7700 </div>` : ''; 7701 7702 // Name dropdown for response form 7703 const currentName = currentProfile ? currentProfile.display_name : ''; 7704 const nameDropdown = ` 7705 <select id="harvest-name-${harvest.id}" style="width:100%;padding:10px 14px;border:1px solid rgba(197,165,90,0.3);border-radius:8px;font-family:'Lora',serif;font-size:13px;margin-bottom:10px;background:var(--cream);color:var(--charcoal);"> 7706 <option value="">Select your name...</option> 7707 <option value="Sumir" ${currentName === 'Sumir' ? 'selected' : ''}>Sumir</option> 7708 <option value="Beth" ${currentName === 'Beth' ? 'selected' : ''}>Beth</option> 7709 <option value="Mindy" ${currentName === 'Mindy' ? 'selected' : ''}>Mindy</option> 7710 </select>`; 7711 7712 return ` 7713 <div style="background:white;border-radius:14px;border:1px solid rgba(197,165,90,0.15);padding:24px;margin-top:12px;"> 7714 <div style="display:flex;justify-content:space-between;align-items:flex-start;"> 7715 <div> 7716 ${harvest.title ? `<div style="font-family:'Cormorant Garamond',serif;font-size:18px;color:var(--plum-deep);margin-bottom:8px;">${escapeHtml(harvest.title)}</div>` : ''} 7717 </div> 7718 ${deleteBtn} 7719 </div> 7720 <div style="font-size:14px;line-height:1.8;color:var(--charcoal);white-space:pre-wrap;">${escapeHtml(harvest.prompt_text)}</div> 7721 7722 <!-- Response form or my response --> 7723 <div style="margin-top:20px;padding-top:16px;border-top:1px solid rgba(197,165,90,0.15);"> 7724 ${myResponse ? ` 7725 <div style="font-family:'Marcellus',serif;font-size:9px;letter-spacing:2px;color:var(--gold);text-transform:uppercase;margin-bottom:8px;">✓ Your harvest has been shared</div> 7726 ` : ` 7727 <div style="font-family:'Marcellus',serif;font-size:9px;letter-spacing:2px;color:var(--gold);text-transform:uppercase;margin-bottom:8px;">Share Your Harvest</div> 7728 ${nameDropdown} 7729 <textarea id="harvest-response-${harvest.id}" placeholder="Take a moment to reflect... What are you harvesting this month?" style="width:100%;padding:14px;border:1px solid rgba(197,165,90,0.3);border-radius:10px;font-family:'Lora',serif;font-size:13px;min-height:100px;resize:vertical;background:var(--cream);line-height:1.7;"></textarea> 7730 <div style="display:flex;justify-content:flex-end;margin-top:10px;"> 7731 <button onclick="submitHarvestResponse('${harvest.id}')" style="background:var(--berry);color:white;border:none;padding:9px 20px;border-radius:8px;font-family:'Marcellus',serif;font-size:9px;letter-spacing:2px;text-transform:uppercase;cursor:pointer;">Share Harvest</button> 7732 </div> 7733 `} 7734 </div> 7735 7736 ${allResponses} 7737 </div>`; 7738} 7739 7740async function createHarvest() { 7741 if (!sbClient || !currentUser) return;
7742 const month = document.getElementById('harvest-month-select').value; 7743 const title = document.getElementById('harvest-title-input').value.trim(); 7744 const prompt = document.getElementById('harvest-prompt-input').value.trim(); 7745 7746 if (!month) { showToast('Please select a month.', 'error'); return; } 7747 if (!prompt) { showToast('Please write a prompt or inquiry.', 'error'); return; } 7748 7749 const { error } = await sbClient.from('monthly_harvests').insert({ 7750 month_label: month, 7751 title: title || null, 7752 prompt_text: prompt, 7753 author_id: currentUser.id 7754 }); 7755 7756 if (error) { 7757 console.error('Harvest create error:', error); 7758 showToast('Could not create harvest: ' + error.message, 'error'); 7759 return; 7760 } 7761 7762 document.getElementById('harvest-month-select').value = ''; 7763 document.getElementById('harvest-title-input').value = ''; 7764 document.getElementById('harvest-prompt-input').value = ''; 7765 loadHarvests(); 7766} 7767 7768async function submitHarvestResponse(harvestId) { 7769 if (!sbClient || !currentUser) return; 7770 const nameSelect = document.getElementById('harvest-name-' + harvestId); 7771 const textarea = document.getElementById('harvest-response-' + harvestId); 7772 const selectedName = nameSelect.value; 7773 const body = textarea.value.trim(); 7774 7775 if (!selectedName) { showToast('Please select your name.', 'error'); return; } 7776 if (!body) { showToast('Please write your reflection before sharing.', 'error'); return; } 7777 7778 const btn = textarea.parentElement?.querySelector('button') || document.querySelector(`button[onclick*="submitHarvestResponse('${harvestId}')"]`); 7779 setButtonLoading(btn, true, 'Sharing...'); 7780 7781 try { 7782 const { error } = await sbClient.from('harvest_responses').insert({ 7783 harvest_id: harvestId, 7784 user_id: currentUser.id, 7785 display_name: selectedName, 7786 body: body, 7787 hearts: 0, 7788 hearted_by: [] 7789 }); 7790 if (error) throw error; 7791 showToast('Your reflection has been shared', 'success'); 7792 loadHarvests(); 7793 } catch (e) { 7794 console.error('Harvest response error:', e); 7795 showToast('Could not save your response: ' + (e.message || e), 'error'); 7796 setButtonLoading(btn, false); 7797 } 7798} 7799 7800async function toggleHarvestHeart(responseId) { 7801 if (!sbClient || !currentUser) return; 7802 7803 // Get current response 7804 const { data: response } = await sbClient 7805 .from('harvest_responses') 7806 .select('hearts, hearted_by') 7807 .eq('id', responseId) 7808 .single(); 7809 7810 if (!response) return; 7811 7812 let heartedBy = response.hearted_by || []; 7813 let hearts = response.hearts || 0; 7814 const alreadyHearted = heartedBy.includes(currentUser.id); 7815 7816 if (alreadyHearted) { 7817 heartedBy = heartedBy.filter(id => id !== currentUser.id); 7818 hearts = Math.max(0, hearts - 1); 7819 } else { 7820 heartedBy.push(currentUser.id); 7821 hearts += 1; 7822 } 7823 7824 await sbClient 7825 .from('harvest_responses') 7826 .update({ hearts, hearted_by: heartedBy }) 7827 .eq('id', responseId); 7828 7829 loadHarvests(); 7830} 7831 7832async function deleteHarvest(harvestId) { 7833 showConfirmModal('Remove this harvest and all its responses?', async () => { 7834 await sbClient.from('harvest_responses').delete().eq('harvest_id', harvestId); 7835 await sbClient.from('monthly_harvests').delete().eq('id', harvestId); 7836 loadHarvests(); 7837 }); 7838} 7839 7840// ============================================================ 7841// MEMBERS & ANALYTICS 7842// ============================================================ 7843async function loadMembers() { 7844 if (!sbClient || !currentProfile || currentProfile.role !== 'teacher') return; 7845 7846 // Load all profiles, EXCLUDING any teacher accounts (so admin activity doesn't skew metrics) 7847 const { data: allProfiles } = await sbClient 7848 .from('profiles') 7849 .select('*') 7850 .order('created_at', { ascending: false }); 7851 7852 if (!allProfiles) return; 7853 7854 // Filter out teacher accounts 7855 const members = allProfiles.filter(m => m.role !== 'teacher'); 7856 7857 // Stats 7858 const now = new Date(); 7859 const sevenDaysAgo = new Date(now - 7 * 24 * 60 * 60 * 1000); 7860 const thirtyDaysAgo = new Date(now - 30 * 24 * 60 * 60 * 1000); 7861 7862 const total = members.length; 7863 const active7 = members.filter(m => m.last_login && new Date(m.last_login) > sevenDaysAgo).length; 7864 const active30 = members.filter(m => m.last_login && new Date(m.last_login) > thirtyDaysAgo).length; 7865 7866 document.getElementById('stat-total').textContent = total; 7867 document.getElementById('stat-active-7').textContent = active7; 7868 document.getElementById('stat-active-30').textContent = active30; 7869 7870 // Members table 7871 const tbody = document.getElementById('members-table-body'); 7872 if (members.length === 0) { 7873 tbody.innerHTML = '<tr><td colspan="4" style="padding:30px;text-align:center;color:var(--charcoal);opacity:0.5;">No members yet</td></tr>'; 7874 return; 7875 } 7876 7877 tbody.innerHTML = members.map(m => { 7878 const joinDate = m.created_at ? new Date(m.created_at).toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' }) : '\u2014'; 7879 const lastActive = m.last_login ? timeAgo(new Date(m.last_login)) : 'Never'; 7880 const roleBadge = m.role === 'teacher' 7881 ? '<span style="background:var(--gold);color:white;padding:2px 8px;border-radius:10px;font-size:10px;font-family:Marcellus,serif;letter-spacing:1px;text-transform:uppercase;">Teacher</span>' 7882 : '<span style="background:var(--cream-warm);color:var(--charcoal);padding:2px 8px;border-radius:10px;font-size:10px;font-family:Marcellus,serif;letter-spacing:1px;text-transform:uppercase;">Member</span>'; 7883 return `<tr style="border-bottom:1px solid rgba(197,165,90,0.1);"> 7884 <td style="padding:12px 18px;font-weight:600;">${escapeHtml(m.display_name || 'Unnamed')}</td> 7885 <td style="padding:12px 18px;opacity:0.7;">${joinDate}</td> 7886 <td style="padding:12px 18px;opacity:0.7;">${lastActive}</td> 7887 <td style="padding:12px 18px;">${roleBadge}</td> 7888 </tr>`; 7889 }).join(''); 7890 7891 // Load library analytics + members 7892 loadLibraryAnalytics(); 7893 loadLibraryMembers(); 7894} 7895 7896// ============================================================ 7897// LIBRARY MEMBERS â who's signing in to the Golden Path Library 7898// ============================================================ 7899async function loadLibraryMembers() { 7900 if (!sbClient || !currentProfile || currentProfile.role !== 'teacher') return; 7901 7902 // Load all library sessions 7903 const { data: sessions } = await sbClient 7904 .from('library_sessions') 7905 .select('user_id, user_email, created_at') 7906 .order('created_at', { ascending: false }); 7907 7908 // Load all library bookmarks (to count favorites per user) 7909 const { data: bookmarks } = await sbClient 7910 .from('library_bookmarks') 7911 .select('user_id, teaching_key'); 7912 7913 // Load all profiles (to map user_id â display_name) 7914 const { data: profiles } = await sbClient 7915 .from('profiles') 7916 .select('id, display_name, role'); 7917 7918 // Build a set of teacher user IDs to EXCLUDE from metrics 7919 const teacherIds = new Set();
7920 (profiles || []).forEach(p => { if (p.role === 'teacher') teacherIds.add(p.id); }); 7921 7922 // Filter out any sessions from teachers (so admin activity doesn't skew metrics) 7923 const filteredSessions = (sessions || []).filter(s => !teacherIds.has(s.user_id)); 7924 7925 if (!filteredSessions || filteredSessions.length === 0) { 7926 document.getElementById('stat-lib-members').textContent = '0'; 7927 document.getElementById('stat-lib-signins').textContent = '0'; 7928 document.getElementById('library-members-body').innerHTML = '<tr><td colspan="4" style="padding:30px;text-align:center;color:var(--charcoal);opacity:0.5;">No library sign-ins yet</td></tr>'; 7929 return; 7930 } 7931 7932 // Build profile lookup 7933 const profileMap = {}; 7934 (profiles || []).forEach(p => { profileMap[p.id] = p; }); 7935 7936 // Count bookmarks per user (also excluding teacher) 7937 const bookmarkCounts = {}; 7938 (bookmarks || []).forEach(b => { 7939 if (teacherIds.has(b.user_id)) return; // skip teacher's bookmarks 7940 bookmarkCounts[b.user_id] = (bookmarkCounts[b.user_id] || 0) + 1; 7941 }); 7942 7943 // Group sessions by user 7944 const userMap = {}; 7945 filteredSessions.forEach(s => { 7946 if (!s.user_id) return; 7947 if (!userMap[s.user_id]) { 7948 userMap[s.user_id] = { 7949 user_id: s.user_id, 7950 email: s.user_email || '', 7951 signins: 0, 7952 last_visit: s.created_at // first one (newest) is last_visit 7953 }; 7954 } 7955 userMap[s.user_id].signins++; 7956 // Keep latest timestamp 7957 if (new Date(s.created_at) > new Date(userMap[s.user_id].last_visit)) { 7958 userMap[s.user_id].last_visit = s.created_at; 7959 } 7960 }); 7961 7962 const members = Object.values(userMap).sort((a, b) => new Date(b.last_visit) - new Date(a.last_visit)); 7963 7964 // Update stat tiles (uses filtered sessions, so teacher activity is excluded) 7965 document.getElementById('stat-lib-members').textContent = members.length; 7966 document.getElementById('stat-lib-signins').textContent = filteredSessions.length; 7967 7968 // Render table rows 7969 document.getElementById('library-members-body').innerHTML = members.map(m => { 7970 const profile = profileMap[m.user_id]; 7971 const displayName = profile?.display_name || ''; 7972 const role = profile?.role; 7973 const isMentorship = role === 'student' || role === 'teacher'; 7974 const lastVisit = timeAgo(new Date(m.last_visit)); 7975 const favCount = bookmarkCounts[m.user_id] || 0; 7976 const tag = isMentorship 7977 ? '<span style="display:inline-block;font-family:Marcellus,serif;font-size:8px;letter-spacing:1.5px;padding:2px 7px;border-radius:8px;background:rgba(148,14,75,0.1);color:var(--berry);margin-left:8px;">MENTORSHIP</span>' 7978 : '<span style="display:inline-block;font-family:Marcellus,serif;font-size:8px;letter-spacing:1.5px;padding:2px 7px;border-radius:8px;background:rgba(197,165,90,0.15);color:var(--gold);margin-left:8px;">LIBRARY ONLY</span>'; 7979 7980 const nameOrEmail = displayName 7981 ? `<span style="font-weight:600;">${escapeHtml(displayName)}</span><br><span style="font-size:11px;opacity:0.7;">${escapeHtml(m.email)}</span>` 7982 : `<span style="font-weight:600;">${escapeHtml(m.email)}</span>`; 7983 7984 return `<tr style="border-bottom:1px solid rgba(197,165,90,0.1);"> 7985 <td style="padding:12px 18px;">${nameOrEmail}${tag}</td> 7986 <td style="padding:12px 18px;text-align:center;font-family:'Marcellus',serif;color:var(--berry);">${m.signins}</td> 7987 <td style="padding:12px 18px;text-align:center;color:var(--berry);">${favCount > 0 ? '♥ ' + favCount : '—'}</td> 7988 <td style="padding:12px 18px;opacity:0.7;font-size:12px;">${lastVisit}</td> 7989 </tr>`; 7990 }).join(''); 7991} 7992 7993function timeAgo(date) { 7994 const seconds = Math.floor((new Date() - date) / 1000); 7995 if (seconds < 60) return 'Just now'; 7996 const minutes = Math.floor(seconds / 60); 7997 if (minutes < 60) return minutes + 'm ago'; 7998 const hours = Math.floor(minutes / 60); 7999 if (hours < 24) return hours + 'h ago'; 8000 const days = Math.floor(hours / 24); 8001 if (days === 1) return 'Yesterday'; 8002 if (days < 7) return days + ' days ago'; 8003 if (days < 30) return Math.floor(days / 7) + 'w ago'; 8004 return date.toLocaleDateString('en-US', { month: 'short', day: 'numeric' }); 8005} 8006 8007const TEACHING_NAMES = { 8008 'teaching-01': 'Insights into Survival, Trauma & Regulation', 8009 'teaching-02': 'Stories & Beliefs', 8010 'teaching-03': 'Consciousness Is Primary',
8011 'teaching-04': '80/20: Returning to Center', 8012 'teaching-05': 'Accepting What Is', 8013 'teaching-06': 'Automatic, Conscious, Simple', 8014 'teaching-07': 'Opening Our Sight', 8015 'teaching-09': 'Rewriting the Code', 8016 'teaching-10': 'The S Practice' 8017}; 8018 8019async function loadLibraryAnalytics() { 8020 try { 8021 const { data: analytics } = await sbClient 8022 .from('library_analytics') 8023 .select('*'); 8024 8025 if (!analytics || analytics.length === 0) { 8026 document.getElementById('stat-lib-views').textContent = '0'; 8027 document.getElementById('stat-lib-prints').textContent = '0'; 8028 document.getElementById('library-analytics-body').innerHTML = '<tr><td colspan="3" style="padding:30px;text-align:center;color:var(--charcoal);opacity:0.5;">No analytics data yet</td></tr>'; 8029 return; 8030 } 8031 8032 const views = analytics.filter(a => a.event_type === 'view'); 8033 const prints = analytics.filter(a => a.event_type === 'print'); 8034 8035 document.getElementById('stat-lib-views').textContent = views.length; 8036 document.getElementById('stat-lib-prints').textContent = prints.length; 8037 8038 // Group by teaching 8039 const teachingStats = {}; 8040 analytics.forEach(a => { 8041 if (!teachingStats[a.teaching_id]) teachingStats[a.teaching_id] = { views: 0, prints: 0 }; 8042 if (a.event_type === 'view') teachingStats[a.teaching_id].views++; 8043 if (a.event_type === 'print') teachingStats[a.teaching_id].prints++; 8044 }); 8045 8046 const sorted = Object.entries(teachingStats).sort((a, b) => b[1].views - a[1].views); 8047 8048 document.getElementById('library-analytics-body').innerHTML = sorted.map(([id, stats]) => { 8049 const name = TEACHING_NAMES[id] || id; 8050 return `<tr style="border-bottom:1px solid rgba(197,165,90,0.1);"> 8051 <td style="padding:12px 18px;">${escapeHtml(name)}</td> 8052 <td style="padding:12px 18px;text-align:center;font-weight:600;">${stats.views}</td> 8053 <td style="padding:12px 18px;text-align:center;font-weight:600;">${stats.prints}</td> 8054 </tr>`; 8055 }).join(''); 8056 } catch(e) { 8057 console.error('Library analytics error:', e); 8058 } 8059} 8060 8061// Voice note player â blob + HTMLAudioElement with duration fix for broken WebM 8062var vnCache = {}; 8063 8064function vnFormatTime(s) { 8065 if (!s || !isFinite(s)) return '0:00'; 8066 var m = Math.floor(s / 60); 8067 var sec = Math.floor(s % 60); 8068 return m + ':' + (sec < 10 ? '0' : '') + sec; 8069} 8070 8071function preloadVoiceNotes() { 8072 document.querySelectorAll('.voice-note-player[data-url]').forEach(function(wrap) { 8073 var id = wrap.id; 8074 var url = wrap.dataset.url; 8075 if (!url || vnCache[id]) return; 8076 vnCache[id] = { loading: true }; 8077 8078 fetch(url) 8079 .then(function(r) { return r.blob(); }) 8080 .then(function(blob) { 8081 // Older notes are WebM (broken duration, no iOS playback); new notes are MP3. 8082 var isWebm = /webm/i.test(blob.type) || /\.webm(\?|$)/i.test(url); 8083 var blobUrl = URL.createObjectURL(blob); 8084 var audio = new Audio(); 8085 audio.preload = 'auto'; 8086 audio.src = blobUrl; 8087 8088 var durationResolved = false; 8089 8090 function finishSetup(dur) { 8091 if (durationResolved) return; 8092 durationResolved = true; 8093 vnCache[id] = { 8094 audio: audio, 8095 blobUrl: blobUrl, 8096 duration: dur, 8097 isWebm: isWebm, 8098 ready: true 8099 }; 8100 audio.currentTime = 0; 8101 var elEl = wrap.querySelector('.vn-elapsed'); 8102 var remEl = wrap.querySelector('.vn-remaining'); 8103 if (elEl) elEl.textContent = '0:00'; 8104 if (remEl) remEl.textContent = vnFormatTime(dur); // full length shown before play 8105
8106 // Wire up events 8107 audio.addEventListener('ended', function() { 8108 var btn = wrap.querySelector('.vn-play-btn'); 8109 btn.innerHTML = '▶︎'; 8110 wrap.querySelector('.vn-progress-bar').style.width = '0%'; 8111 wrap.querySelector('.vn-elapsed').textContent = '0:00'; 8112 wrap.querySelector('.vn-remaining').textContent = vnFormatTime(dur); 8113 if (vnCache[id].rafId) cancelAnimationFrame(vnCache[id].rafId); 8114 }); 8115 } 8116 8117 audio.addEventListener('loadedmetadata', function() { 8118 if (audio.duration && isFinite(audio.duration) && audio.duration > 0 && audio.duration < 86400) { 8119 finishSetup(audio.duration); 8120 } else if (isWebm) { 8121 // Broken WebM duration â seek to end to force the browser to find it 8122 audio.currentTime = 1e7; 8123 } 8124 }); 8125 8126 audio.addEventListener('timeupdate', function onSeekEnd() { 8127 if (durationResolved) return; 8128 if (audio.duration && isFinite(audio.duration) && audio.duration > 0 && audio.duration < 86400) { 8129 audio.removeEventListener('timeupdate', onSeekEnd); 8130 finishSetup(audio.duration); 8131 } 8132 }); 8133 8134 // Fallback timeout â if duration still not resolved after 5s, use what we have 8135 setTimeout(function() { 8136 if (!durationResolved) { 8137 var dur = (audio.duration && isFinite(audio.duration)) ? audio.duration : 300; 8138 console.warn('[VoiceNote ' + id + '] Duration fallback: ' + dur); 8139 finishSetup(dur); 8140 } 8141 }, 5000); 8142 }) 8143 .catch(function(err) { 8144 console.error('Voice note preload failed:', id, err); 8145 vnCache[id] = { failed: true }; 8146 }); 8147 }); 8148} 8149 8150function vnUpdateProgress(id) { 8151 var c = vnCache[id]; 8152 if (!c || !c.audio) return; 8153 var audio = c.audio; 8154 if (audio.paused && !audio.ended) return; 8155 var wrap = document.getElementById(id); 8156 if (!wrap) return; 8157 var pct = c.duration > 0 ? (audio.currentTime / c.duration * 100) : 0; 8158 if (pct > 100) pct = 100; 8159 wrap.querySelector('.vn-progress-bar').style.width = pct + '%'; 8160 wrap.querySelector('.vn-elapsed').textContent = vnFormatTime(audio.currentTime); 8161 wrap.querySelector('.vn-remaining').textContent = '-' + vnFormatTime(Math.max(0, c.duration - audio.currentTime)); 8162 if (!audio.paused && !audio.ended) { 8163 c.rafId = requestAnimationFrame(function() { vnUpdateProgress(id); }); 8164 } 8165} 8166 8167function vnToggle(id) { 8168 var wrap = document.getElementById(id); 8169 if (!wrap) return; 8170 var btn = wrap.querySelector('.vn-play-btn'); 8171 var c = vnCache[id]; 8172 if (!c || !c.ready) { btn.innerHTML = '⌛'; return; } 8173 8174 var audio = c.audio; 8175 8176 // If playing, pause 8177 if (!audio.paused) { 8178 audio.pause(); 8179 btn.innerHTML = '▶︎'; 8180 if (c.rafId) cancelAnimationFrame(c.rafId); 8181 return; 8182 } 8183 8184 // Pause all others 8185 Object.keys(vnCache).forEach(function(k) { 8186 if (k !== id && vnCache[k].audio && !vnCache[k].audio.paused) { 8187 vnCache[k].audio.pause(); 8188 var otherWrap = document.getElementById(k); 8189 if (otherWrap) { 8190 otherWrap.querySelector('.vn-play-btn').innerHTML = '▶︎'; 8191 if (vnCache[k].rafId) cancelAnimationFrame(vnCache[k].rafId); 8192 } 8193 } 8194 }); 8195 8196 // WebM files need a tiny nudge past zero to start; MP3 plays cleanly from 0 8197 if (c.isWebm && audio.currentTime < 0.01) audio.currentTime = 0.01; 8198 audio.play().then(function() { 8199 btn.innerHTML = '▮▮'; 8200 vnUpdateProgress(id); 8201 }).catch(function(err) { 8202 console.error('Play failed:', err); 8203 btn.innerHTML = '▶︎'; 8204 }); 8205} 8206 8207function vnSeek(event, id) { 8208 var c = vnCache[id]; 8209 if (!c || !c.ready) return; 8210 var track = document.getElementById(id).querySelector('.vn-progress-track'); 8211 var rect = track.getBoundingClientRect(); 8212 var pct = Math.max(0, Math.min(1, (event.clientX - rect.left) / rect.width)); 8213 var seekTo = pct * c.duration; 8214 var wasPlaying = !c.audio.paused; 8215 8216 c.audio.currentTime = seekTo; 8217 8218 var wrap = document.getElementById(id); 8219 wrap.querySelector('.vn-progress-bar').style.width = (pct * 100) + '%'; 8220 wrap.querySelector('.vn-elapsed').textContent = vnFormatTime(seekTo); 8221 wrap.querySelector('.vn-remaining').textContent = '-' + vnFormatTime(Math.max(0, c.duration - seekTo)); 8222 8223 if (wasPlaying) vnUpdateProgress(id); 8224} 8225 8226// ============================================================ 8227// ONE-TIME LEGACY AUDIO CONVERTER (WebM -> MP3) 8228// Teacher-only. Decodes old WebM notes in-browser (native decode) and 8229// re-encodes to MP3 with lamejs so they play on every device incl. iPhone. 8230// ============================================================ 8231async function findLegacyVoiceNotes() { 8232 if (!sbClient) return []; 8233 const { data, error } = await sbClient 8234 .from('posts') 8235 .select('id, media_url, media_type, post_type') 8236 .or('media_type.eq.audio,post_type.eq.voice_note'); 8237 if (error) { console.error('Legacy scan failed:', error); return []; } 8238 return (data || []).filter(p => p.media_url && /\.webm(\?|$)/i.test(p.media_url)); 8239} 8240 8241async function checkLegacyVoiceNotes() { 8242 // Only teachers can run the migration (and only they can update others' posts) 8243 if (!currentProfile || currentProfile.role !== 'teacher') return; 8244 const banner = document.getElementById('legacy-audio-banner'); 8245 if (!banner) return; 8246 const legacy = await findLegacyVoiceNotes(); 8247 if (legacy.length > 0) { 8248 document.getElementById('legacy-audio-text').innerHTML = 8249 legacy.length + ' older voice note' + (legacy.length === 1 ? '' : 's') + 8250 ' use a format that doesn’t play on iPhones. Convert to MP3 so every sister can listen on any device. Your original posts stay in place — only the audio format is upgraded.'; 8251 banner.style.display = 'block'; 8252 } else { 8253 banner.style.display = 'none'; 8254 } 8255} 8256 8257async function convertOneWebmToMp3(url) { 8258 // Fetch the WebM bytes and decode natively (Chrome decodes Opus/WebM) 8259 const resp = await fetch(url.split('#')[0]); 8260 const buf = await resp.arrayBuffer(); 8261 const ctx = new (window.AudioContext || window.webkitAudioContext)(); 8262 const audioBuffer = await ctx.decodeAudioData(buf); 8263 8264 // Downmix to mono Float32 8265 const len = audioBuffer.length; 8266 const mono = new Float32Array(len); 8267 const chs = audioBuffer.numberOfChannels; 8268 for (let c = 0; c < chs; c++) { 8269 const d = audioBuffer.getChannelData(c); 8270 for (let i = 0; i < len; i++) mono[i] += d[i] / chs; 8271 } 8272 const sampleRate = Math.round(audioBuffer.sampleRate); 8273 try { ctx.close(); } catch (e) {} 8274 8275 // Encode to MP3 (mono, 96kbps) 8276 const encoder = new lamejs.Mp3Encoder(1, sampleRate, 96); 8277 const block = 1152; 8278 const mp3Data = []; 8279 const samples = new Int16Array(len); 8280 for (let i = 0; i < len; i++) { 8281 const s = Math.max(-1, Math.min(1, mono[i])); 8282 samples[i] = s < 0 ? s * 0x8000 : s * 0x7FFF; 8283 } 8284 for (let i = 0; i < samples.length; i += block) { 8285 const chunk = samples.subarray(i, i + block); 8286 const enc = encoder.encodeBuffer(chunk); 8287 if (enc.length > 0) mp3Data.push(new Int8Array(enc)); 8288 } 8289 const end = encoder.flush(); 8290 if (end.length > 0) mp3Data.push(new Int8Array(end)); 8291 return new Blob(mp3Data, { type: 'audio/mpeg' }); 8292} 8293 8294async function convertLegacyVoiceNotes() { 8295 const btn = document.getElementById('legacy-audio-btn'); 8296 if (!btn || btn.disabled) return; 8297 if (!currentProfile || currentProfile.role !== 'teacher') { 8298 showToast('Only a teacher can run this conversion.', 'error'); 8299 return; 8300 } 8301 btn.disabled = true; 8302 const original = btn.innerHTML; 8303 8304 const legacy = await findLegacyVoiceNotes(); 8305 if (legacy.length === 0) { 8306 document.getElementById('legacy-audio-banner').style.display = 'none'; 8307 return; 8308 } 8309 8310 let done = 0, failed = 0; 8311 for (let i = 0; i < legacy.length; i++) { 8312 const post = legacy[i];
8313 btn.innerHTML = 'Converting ' + (i + 1) + ' of ' + legacy.length + 'â¦'; 8314 try { 8315 const mp3Blob = await convertOneWebmToMp3(post.media_url); 8316 const path = currentUser.id + '/converted-' + post.id + '-' + Date.now() + '.mp3'; 8317 const { error: upErr } = await sbClient.storage 8318 .from('Media') 8319 .upload(path, mp3Blob, { cacheControl: '3600', upsert: true, contentType: 'audio/mpeg' }); 8320 if (upErr) throw upErr; 8321 8322 const { data: urlData } = sbClient.storage.from('Media').getPublicUrl(path); 8323 8324 const { error: updErr } = await sbClient 8325 .from('posts') 8326 .update({ media_url: urlData.publicUrl, media_type: 'audio' }) 8327 .eq('id', post.id); 8328 if (updErr) throw updErr; 8329 8330 done++; 8331 } catch (e) { 8332 console.error('Convert failed for post ' + post.id, e); 8333 failed++; 8334 } 8335 } 8336 8337 btn.innerHTML = original; 8338 btn.disabled = false; 8339 8340 if (failed === 0) { 8341 showToast('Converted ' + done + ' voice note' + (done === 1 ? '' : 's') + ' to MP3. They now play on every device.', 'success'); 8342 document.getElementById('legacy-audio-banner').style.display = 'none'; 8343 // Clear cached players and reload the feed so the new MP3s render 8344 vnCache = {}; 8345 if (typeof loadFeed === 'function') loadFeed(); 8346 } else { 8347 showToast('Converted ' + done + ', but ' + failed + ' failed. Check the console and try again.', 'error'); 8348 } 8349} 8350</script>
8350 8351 8352<form method='POST' name='new-member-signup' style='display:none;'> 8353 <input type="hidden" name="form-name" value="new-member-signup"> 8354 <input type="hidden" name="member-name" id="notify-member-name"> 8355 <input type="hidden" name="member-email" id="notify-member-email"> 8356 <input type="hidden" name="signup-date" id="notify-signup-date"> 8357</form> 8358 8359</body> 8360</html>
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.