<!doctype html>
<html lang="en" translate="no">
  <head>
    <meta charset="UTF-8" />
    <!-- `initial-scale=1` is not optional. Without it Safari/WKWebView (the
         iOS app included) pick their own starting scale from the layout width
         they infer, and the app opens permanently zoomed in — you have to
         pinch out on every page, every time (Eliau, 2026-08-10). It pins the
         first paint at 1:1 and nothing else: `user-scalable=yes` stays, and no
         `maximum-scale` is set, so pinch-to-zoom keeps working everywhere
         (accessibility, and the PDF pages people zoom into). -->
    <meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=yes, viewport-fit=cover" />
    <!-- Chrome's built-in page translator mutates the DOM (wraps text nodes
         in <font> tags) behind React's back. When that happens mid-render,
         React's next commit can throw "Failed to execute 'removeChild': the
         node to be removed is not a child of this node" and crash the whole
         app — a well-documented React/Chrome-Translate conflict. This app
         already ships real es/he/en translations via i18next, so the
         browser's own auto-translate offers no value here and only adds
         risk — opt every page out of it. -->
    <meta name="google" content="notranslate" />
    <title>Yafutzu — Torah Study & the Jewish Social Network for Every Jew</title>
    <link rel="canonical" href="https://www.yafutzu.org/" />

    <!-- Primary Meta -->
    <meta name="description" content="Yafutzu — Torah study and the Jewish social network. Find a minyan, map a kosher trip, meet a chavruta, learn daily, connect everywhere." />
    <meta name="author" content="Yafutzu" />

    <!-- Open Graph -->
    <meta property="og:type" content="website" />
    <meta property="og:url" content="https://www.yafutzu.org/" />
    <meta property="og:title" content="Yafutzu — Torah Study and the Jewish World, in One App" />
    <meta property="og:description" content="Yafutzu connects every Jew through Torah study and the Jewish social network. Learn daily, find a minyan, navigate the kosher world, meet a chavruta, plan Jewish travel — one app, every Jew, everywhere." />
    <meta property="og:image" content="https://yafutzu.s3.us-east-2.amazonaws.com/Logos/yafutzu_logo.jpg" />
    <meta property="og:image:width" content="1200" />
    <meta property="og:image:height" content="630" />
    <meta property="og:site_name" content="Yafutzu" />

    <!-- Twitter Card -->
    <meta name="twitter:card" content="summary_large_image" />
    <meta name="twitter:title" content="Yafutzu — Torah, Community & the Jewish World, in One App" />
    <meta name="twitter:description" content="Torah study, minyanim, kosher map, chavruta, travel — Yafutzu connects every Jew, everywhere." />
    <meta name="twitter:image" content="https://yafutzu.s3.us-east-2.amazonaws.com/Logos/yafutzu_logo.jpg" />

    <!-- Theme & PWA -->
    <meta name="theme-color" content="#0A1B28" />
    <meta name="apple-mobile-web-app-capable" content="yes" />
    <meta name="apple-mobile-web-app-status-bar-style" content="default" />
    <meta name="apple-mobile-web-app-title" content="יפוצו" />
    <link rel="manifest" href="/manifest.json" />
    <script>
      // ── PWA install capture ────────────────────────────────────────────
      // `beforeinstallprompt` fires ONCE, early, and cannot be replayed — a
      // React component that mounts after it has simply missed the only
      // chance to offer an install. So we catch it here, before the bundle
      // even loads, and park it on window for src/lib/pwaInstall.js to read.
      window.__yfInstall = { event: null, installed: false };
      window.addEventListener('beforeinstallprompt', function (e) {
        e.preventDefault(); // suppress Chrome's mini-infobar; we ask in-app
        window.__yfInstall.event = e;
        window.dispatchEvent(new Event('yf:installable'));
      });
      window.addEventListener('appinstalled', function () {
        window.__yfInstall.event = null;
        window.__yfInstall.installed = true;
        window.dispatchEvent(new Event('yf:installed'));
      });
    </script>
    <link rel="icon" href="/favicon.ico" sizes="any" />
    <link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png" />
    <link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png" />
    <link rel="icon" type="image/svg+xml" href="/icons/yafutzu-logo.svg" />
    <link rel="apple-touch-icon" href="/apple-touch-icon.png" />
    <!-- iOS splash screens — device-sized startup images so the PWA opens with a branded splash, not a white flash -->
    <!-- iPhone 14 Pro Max, 15 Pro Max, 16 Pro Max -->
    <link rel="apple-touch-startup-image" href="/icons/yafutzu-logo.svg" media="(device-width: 430px) and (device-height: 932px) and (-webkit-device-pixel-ratio: 3)" />
    <!-- iPhone 14 Pro, 15 Pro, 16 Pro -->
    <link rel="apple-touch-startup-image" href="/icons/yafutzu-logo.svg" media="(device-width: 393px) and (device-height: 852px) and (-webkit-device-pixel-ratio: 3)" />
    <!-- iPhone 14 Plus, 13 Pro Max, 12 Pro Max -->
    <link rel="apple-touch-startup-image" href="/icons/yafutzu-logo.svg" media="(device-width: 428px) and (device-height: 926px) and (-webkit-device-pixel-ratio: 3)" />
    <!-- iPhone 14, 13, 13 Pro, 12, 12 Pro -->
    <link rel="apple-touch-startup-image" href="/icons/yafutzu-logo.svg" media="(device-width: 390px) and (device-height: 844px) and (-webkit-device-pixel-ratio: 3)" />
    <!-- iPhone 13 mini, 12 mini, 11 Pro, XS, X -->
    <link rel="apple-touch-startup-image" href="/icons/yafutzu-logo.svg" media="(device-width: 375px) and (device-height: 812px) and (-webkit-device-pixel-ratio: 3)" />
    <!-- iPhone 11 Pro Max, XS Max -->
    <link rel="apple-touch-startup-image" href="/icons/yafutzu-logo.svg" media="(device-width: 414px) and (device-height: 896px) and (-webkit-device-pixel-ratio: 3)" />
    <!-- iPhone 11, XR -->
    <link rel="apple-touch-startup-image" href="/icons/yafutzu-logo.svg" media="(device-width: 414px) and (device-height: 896px) and (-webkit-device-pixel-ratio: 2)" />
    <!-- iPhone 8 Plus, 7 Plus, 6s Plus, 6 Plus -->
    <link rel="apple-touch-startup-image" href="/icons/yafutzu-logo.svg" media="(device-width: 414px) and (device-height: 736px) and (-webkit-device-pixel-ratio: 3)" />
    <!-- iPhone SE (2nd/3rd gen), 8, 7, 6s, 6 -->
    <link rel="apple-touch-startup-image" href="/icons/yafutzu-logo.svg" media="(device-width: 375px) and (device-height: 667px) and (-webkit-device-pixel-ratio: 2)" />
    <!-- iPad Pro 12.9 -->
    <link rel="apple-touch-startup-image" href="/icons/yafutzu-logo.svg" media="(device-width: 1024px) and (device-height: 1366px) and (-webkit-device-pixel-ratio: 2)" />
    <!-- iPad Pro 11 -->
    <link rel="apple-touch-startup-image" href="/icons/yafutzu-logo.svg" media="(device-width: 834px) and (device-height: 1194px) and (-webkit-device-pixel-ratio: 2)" />
    <!-- iPad Air, iPad 10.2 -->
    <link rel="apple-touch-startup-image" href="/icons/yafutzu-logo.svg" media="(device-width: 820px) and (device-height: 1180px) and (-webkit-device-pixel-ratio: 2)" />
    <!-- Fallback -->
    <link rel="apple-touch-startup-image" href="/icons/yafutzu-logo.svg" />
    <meta name="mobile-web-app-capable" content="yes" />
    <meta name="apple-touch-fullscreen" content="yes" />
    <link rel="preconnect" href="https://fonts.googleapis.com">
    <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
    <!-- Yafutzu type system. The 5 CORE UI fonts — Inter Tight (locked, brand),
         Inter (legacy UI), Frank Ruhl Libre (primary Hebrew), David Libre (alt
         Hebrew), Heebo (UI Hebrew) — are SELF-HOSTED in /fonts/ui/ (see
         src/styles/ui-fonts.css). They were pulled off the Google CDN because a
         privacy/ad-blocker extension stripping fonts.gstatic.com would flatten
         ALL app typography to the system fallback at once. The specialty reader
         fonts below stay on the CDN (font-display:swap; non-critical). -->
    <link rel="preload" as="font" type="font/woff2" href="/fonts/ui/inter-tight-400-900-latin.woff2" crossorigin>
    <link rel="preload" as="font" type="font/woff2" href="/fonts/ui/frank-ruhl-libre-300-900-hebrew.woff2" crossorigin>
    <!-- ⚠️ LOAD THIS NON-BLOCKING — do NOT "simplify" it back to a plain
         rel="stylesheet".

         A plain <link rel="stylesheet"> in <head> is RENDER-BLOCKING: the
         browser will not paint a single pixel until it has resolved DNS,
         completed a TLS handshake, and downloaded this file — from a THIRD
         PARTY origin, before it even starts on our own JS. Measured at 610ms
         from a warm sandbox with DNS already cached; on a phone on mobile
         data, cold, it is routinely 1s+. That is 1s of blank screen paid by
         every visitor before our own loading globe is allowed to appear.

         And it buys nothing for that first screen. These 14 families are the
         SPECIALTY READER fonts (Crimson, Lora, Merriweather, Vollkorn, …)
         offered in the Torah reader's font picker. The home screen does not
         render a single character in any of them — the 5 core UI fonts are
         self-hosted in /fonts/ui/ (see the note above).

         media="print" is the standard trick: the browser sees a stylesheet
         that doesn't apply to the screen, so it fetches it at low priority
         WITHOUT blocking render; onload flips it to media="all" and the
         fonts apply the moment they land. Combined with font-display:swap
         already in the URL, a reader opened in the first second shows its
         fallback face and swaps — which is exactly what swap is for. -->
    <link href="https://fonts.googleapis.com/css2?family=Crimson+Text:ital,wght@0,400;0,600;1,400;1,600&family=Crimson+Pro:ital,wght@0,400;0,500;0,600;0,700;1,400;1,500&family=Source+Serif+4:opsz,wght@8..60,400;8..60,500;8..60,600;8..60,700&family=Lora:ital,wght@0,400;0,500;0,600;0,700;1,400;1,500&family=Merriweather:ital,wght@0,300;0,400;0,700;1,300;1,400&family=Open+Sans:ital,wght@0,400;0,500;0,600;0,700;1,400;1,500&family=Roboto:ital,wght@0,300;0,400;0,500;0,700;1,300;1,400&family=Nunito:ital,wght@0,400;0,500;0,600;0,700;1,400;1,500&family=Newsreader:opsz,wght@6..72,400;6..72,500;6..72,600;6..72,700&family=Vollkorn:ital,wght@0,400;0,500;0,600;0,700;1,400&family=Atkinson+Hyperlegible:ital,wght@0,400;0,700;1,400;1,700&family=Rubik:wght@400;500;700&family=Suez+One&family=Noto+Serif+Hebrew:wght@400;500;600;700;800;900&display=swap" rel="stylesheet" media="print" onload="this.media='all';this.onload=null">
    <!-- Sitewide organization identity — read by search engines + AI models on
         every page (SSR handlers strip title/canonical/og but keep JSON-LD).
         nonprofitStatus: Yafutzu is a registered 501(c)(3); donations carry
         zero platform fees (100% reaches the cause). -->
    <script type="application/ld+json">
    {
      "@context": "https://schema.org",
      "@type": "NonprofitOrganization",
      "name": "Yafutzu",
      "url": "https://www.yafutzu.org",
      "logo": "https://yafutzu.s3.us-east-2.amazonaws.com/Logos/yafutzu_logo.jpg",
      "nonprofitStatus": "Nonprofit501c3",
      "email": "info@yafutzu.org",
      "description": "Torah study and the Jewish social network in one app. A registered 501(c)(3) nonprofit — tzedaka given through Yafutzu reaches its cause with zero platform fees.",
      "slogan": "United we",
      "knowsAbout": ["Torah", "Tzedaka", "Jewish charity", "Maaser", "Chomesh", "Zmanim", "Kosher", "Jewish communities"],
      "potentialAction": {
        "@type": "DonateAction",
        "target": "https://www.yafutzu.org/give",
        "recipient": { "@type": "NonprofitOrganization", "name": "Yafutzu" }
      }
    }
    </script>
    <script type="module" crossorigin src="/assets/index-CK-9rRPh.js"></script>
    <link rel="modulepreload" crossorigin href="/assets/vendor-i18n-CcfN-CjO.js">
    <link rel="modulepreload" crossorigin href="/assets/vendor-react-BgixnT7m.js">
    <link rel="modulepreload" crossorigin href="/assets/vendor-query-DQtbyxfE.js">
    <link rel="modulepreload" crossorigin href="/assets/vendor-ui-PFMdFjjO.js">
    <link rel="modulepreload" crossorigin href="/assets/vendor-dates-CCRft2q4.js">
    <link rel="modulepreload" crossorigin href="/assets/vendor-socket-BcmHMtos.js">
    <link rel="stylesheet" crossorigin href="/assets/index-D4NM09fB.css">
  </head>
  <body>
    <div id="root">
      <!-- Pre-JS app shell. Paints at ~TTFB with zero JS/asset dependency so
           users see branding instantly instead of a blank screen during the
           ~3s bundle parse/execute. React's createRoot() clears #root on its
           first render, so this is auto-removed the moment the app mounts.
           No hydration (createRoot, not hydrateRoot) → no mismatch risk.

           This SVG is a static copy of YafutzuLoader.jsx (can't import a React
           component here) — keep the two in sync if the animation changes.
           After the app mounts, LoadingScreen renders the real YafutzuLoader
           at the same size/position, so the handoff reads as ONE continuous
           loader instead of two different splash designs flashing. -->
      <div id="yf-splash" style="position:fixed;inset:0;display:flex;align-items:center;justify-content:center;background:#fff;z-index:2147483647">
        <svg viewBox="0 0 512 512" width="148" height="148" fill="none" xmlns="http://www.w3.org/2000/svg" aria-label="Loading…">
          <defs>
            <clipPath id="yfl-splash-clip"><circle cx="256" cy="256" r="210" /></clipPath>
          </defs>
          <g clip-path="url(#yfl-splash-clip)" opacity="0.18">
            <path d="M256 46 Q326 256 256 466" stroke="#1E88B0" stroke-width="9" fill="none" />
            <path d="M256 46 Q186 256 256 466" stroke="#1E88B0" stroke-width="9" fill="none" />
            <path d="M256 46 Q390 156 390 256 Q390 356 256 466" stroke="#1E88B0" stroke-width="9" fill="none" />
            <path d="M256 46 Q122 156 122 256 Q122 356 256 466" stroke="#1E88B0" stroke-width="9" fill="none" />
            <ellipse cx="256" cy="256" rx="210" ry="70" stroke="#1E88B0" stroke-width="9" fill="none" />
            <ellipse cx="256" cy="256" rx="210" ry="140" stroke="#1E88B0" stroke-width="9" fill="none" />
          </g>
          <g clip-path="url(#yfl-splash-clip)">
            <path d="M256 46 Q326 256 256 466" fill="none" stroke="#1E88B0" stroke-width="9" stroke-linecap="round" stroke-dasharray="80 760" style="animation:yfl-splash-m1 1.8s ease-in-out infinite;animation-delay:0s" />
            <path d="M256 46 Q186 256 256 466" fill="none" stroke="#1E88B0" stroke-width="9" stroke-linecap="round" stroke-dasharray="80 760" style="animation:yfl-splash-m2 2.1s ease-in-out infinite;animation-delay:0.4s" />
            <path d="M256 46 Q390 156 390 256 Q390 356 256 466" fill="none" stroke="#1E88B0" stroke-width="9" stroke-linecap="round" stroke-dasharray="70 770" style="animation:yfl-splash-m3 2.4s ease-in-out infinite;animation-delay:0.9s" />
            <path d="M256 46 Q122 156 122 256 Q122 356 256 466" fill="none" stroke="#1E88B0" stroke-width="9" stroke-linecap="round" stroke-dasharray="70 770" style="animation:yfl-splash-m4 1.9s ease-in-out infinite;animation-delay:1.4s" />
            <ellipse cx="256" cy="256" rx="210" ry="70" fill="none" stroke="#1E88B0" stroke-width="9" stroke-linecap="round" stroke-dasharray="90 1230" style="animation:yfl-splash-eq 2.6s ease-in-out infinite;animation-delay:0.2s" />
            <ellipse cx="256" cy="256" rx="210" ry="140" fill="none" stroke="#1E88B0" stroke-width="9" stroke-linecap="round" stroke-dasharray="80 1000" style="animation:yfl-splash-l2 2.2s ease-in-out infinite;animation-delay:1.1s" />
          </g>
          <circle cx="256" cy="256" r="210" stroke="#1E88B0" stroke-width="14" fill="none" />
          <line x1="40" y1="256" x2="74" y2="256" stroke="#1E88B0" stroke-width="9" stroke-linecap="round" />
          <line x1="438" y1="256" x2="472" y2="256" stroke="#1E88B0" stroke-width="9" stroke-linecap="round" />
          <text x="256" y="284" text-anchor="middle" font-family="'Heebo', sans-serif" font-size="108" font-weight="700" fill="#1E88B0">יפוצו</text>
        </svg>
      </div>
      <style>
        @keyframes yfl-splash-m1 { from { stroke-dashoffset: 0 } to { stroke-dashoffset: -840 } }
        @keyframes yfl-splash-m2 { from { stroke-dashoffset: 0 } to { stroke-dashoffset: -840 } }
        @keyframes yfl-splash-m3 { from { stroke-dashoffset: 0 } to { stroke-dashoffset: -840 } }
        @keyframes yfl-splash-m4 { from { stroke-dashoffset: 0 } to { stroke-dashoffset: -840 } }
        @keyframes yfl-splash-eq { from { stroke-dashoffset: 0 } to { stroke-dashoffset: -1320 } }
        @keyframes yfl-splash-l2 { from { stroke-dashoffset: 0 } to { stroke-dashoffset: -1080 } }
      </style>
    </div>
    <script>
      // Register service worker + keep it current across deploys.
      // When a new SW is found, tell it to skipWaiting() so the new worker
      // takes over without the PWA being closed — critical for mobile PWA
      // installs where users don't see browser refresh cues.
      //
      // NO RELOADS FROM HERE — not on controllerchange, not when the tab is
      // hidden. (2026-09-06, Eliau: "every first time you go into Yafutzu it
      // loads forever instead of going straight in".) The old handler reloaded
      // the page the moment it was hidden after a new SW activated, and this
      // repo deploys many times a day — so nearly every background→foreground
      // on a phone landed on a page mid-reload: splash, full bundle download,
      // auth, home feed, all over again. A new worker taking control does not
      // make the running page wrong; sw.js serves this page's hashed chunks
      // cache-first, so the build in memory keeps working until the NEXT open,
      // which boots the new build instantly from the cache the worker already
      // filled. If an old chunk truly cannot be served, lazyWithRetry.js +
      // installChunkRecovery() do ONE recovery reload — that is the only
      // reload path left, and it is the exception, not the routine.
      if ('serviceWorker' in navigator) {
        window.addEventListener('load', () => {
          // updateViaCache:'none' tells the browser to bypass its own HTTP
          // cache when fetching sw.js — otherwise browsers cache the SW for
          // up to 24h by default, which means deploys don't reach PWA users
          // until the next day. Critical for iOS Safari + Android Chrome.
          navigator.serviceWorker.register('/sw.js', { updateViaCache: 'none' }).then((reg) => {
            if (!reg) return;

            // Check for updates whenever the tab/PWA becomes visible. This
            // is the real fix — PWAs on mobile get backgrounded constantly,
            // and `reg.update()` on visibility change picks up new deploys
            // within seconds of the user reopening the app.
            document.addEventListener('visibilitychange', () => {
              if (document.visibilityState === 'visible') {
                reg.update().catch(() => {});
              }
            });

            // Fallback poll every 5 minutes while the tab is open (was 30).
            setInterval(() => { reg.update().catch(() => {}); }, 5 * 60 * 1000);

            // When a new SW installs, activate it immediately. Its install
            // step already fetched the new shell and its eager assets into
            // the cache, so the next open boots the new build with no
            // network on the critical path.
            reg.addEventListener('updatefound', () => {
              const nextSW = reg.installing;
              if (!nextSW) return;
              nextSW.addEventListener('statechange', () => {
                if (nextSW.state === 'installed' && navigator.serviceWorker.controller) {
                  nextSW.postMessage({ type: 'SKIP_WAITING' });
                }
              });
            });

            // The worker says a newer build is cached and ready for the next
            // open. Nothing reloads; a future in-app nudge can listen for this.
            navigator.serviceWorker.addEventListener('message', (ev) => {
              if (ev.data && ev.data.type === 'SHELL_UPDATED') {
                window.__yfShellUpdated = true;
                window.dispatchEvent(new Event('yf:shell-updated'));
              }
            });
          }).catch(() => {});
        });
      }
    </script>
  </body>
</html>
