<!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" />
    <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-CPikYwCv.js"></script>
    <link rel="modulepreload" crossorigin href="/assets/vendor-i18n-CcfN-CjO.js">
    <link rel="modulepreload" crossorigin href="/assets/vendor-react-BrB4CSk1.js">
    <link rel="modulepreload" crossorigin href="/assets/vendor-query-DQtbyxfE.js">
    <link rel="modulepreload" crossorigin href="/assets/vendor-dates-CpU9C-Xi.js">
    <link rel="modulepreload" crossorigin href="/assets/vendor-ui-PFMdFjjO.js">
    <link rel="modulepreload" crossorigin href="/assets/vendor-socket-BcmHMtos.js">
    <link rel="stylesheet" crossorigin href="/assets/index-7qWNAfg_.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 + auto-update on new deploys.
      // When a new SW is found, tell it to skipWaiting() so users get the
      // fresh app shell without needing to close the PWA — critical for
      // mobile PWA installs where users don't see browser refresh cues.
      if ('serviceWorker' in navigator) {
        // Was this page already under a service worker's control when it
        // loaded? This single bit is what separates "a NEW VERSION took over,
        // the JS in memory is now stale" from "the very first SW just
        // installed and claimed us, nothing is stale at all" — and getting it
        // wrong cost every first-time visitor a full duplicate page load.
        // See the controllerchange handler below.
        var hadControllerAtLoad = !!navigator.serviceWorker.controller;

        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 so the next
            // navigation serves fresh HTML + assets.
            reg.addEventListener('updatefound', () => {
              const nextSW = reg.installing;
              if (!nextSW) return;
              nextSW.addEventListener('statechange', () => {
                if (nextSW.state === 'installed' && navigator.serviceWorker.controller) {
                  // New version ready — activate + reload on next navigation.
                  nextSW.postMessage({ type: 'SKIP_WAITING' });
                }
              });
            });

            // ── Reload policy on controllerchange ────────────────────────
            //
            // This handler used to reload UNCONDITIONALLY, which was wrong in
            // two different ways and produced most of the "Yafutzu loads too
            // many times / gets stuck on the first loading" reports:
            //
            //   1. FIRST VISIT (the expensive one). sw.js calls skipWaiting()
            //      in install and clients.claim() in activate, so on a brand
            //      new visitor the very first SW claims the page ~380ms in —
            //      while the ~450KB entry bundle is still downloading. That
            //      fired controllerchange, we reloaded, and the visitor threw
            //      away a nearly-complete load and started over from zero.
            //      Every first-time visitor paid the whole cost TWICE.
            //      There is nothing stale on a first install — the page and
            //      the SW that just claimed it are from the same deploy — so
            //      this reload was pure waste. hadControllerAtLoad gates it.
            //
            //   2. LIVE TABS ON EVERY DEPLOY. reg.update() runs on every
            //      visibilitychange, and this repo ships to main many times a
            //      day. So the old code yanked the page out from under anyone
            //      who tabbed back after a deploy — mid-shiur, mid-read,
            //      mid-call. We now wait until the tab is hidden, so the
            //      refresh is invisible: the user comes back to a fresh build
            //      instead of watching one happen. If the tab is never
            //      hidden, lazyWithRetry.js still recovers a stale chunk on
            //      the next route change, so nobody gets stranded.
            let reloaded = false;
            const mediaIsPlaying = () =>
              Array.prototype.some.call(
                document.querySelectorAll('video, audio'),
                (el) => !el.paused && !el.ended && el.readyState > 2
              );

            const reloadForUpdate = () => {
              if (reloaded) return;
              // Never interrupt a live call or a playing shiur, even hidden.
              if (mediaIsPlaying()) return;
              reloaded = true;
              window.location.reload();
            };

            navigator.serviceWorker.addEventListener('controllerchange', () => {
              if (!hadControllerAtLoad) return; // first install claiming us — nothing to refresh
              if (document.visibilityState === 'hidden') return reloadForUpdate();
              // Visible: defer until the user looks away.
              document.addEventListener('visibilitychange', function onHide() {
                if (document.visibilityState !== 'hidden') return;
                document.removeEventListener('visibilitychange', onHide);
                reloadForUpdate();
              });
            });
          }).catch(() => {});
        });
      }
    </script>
  </body>
</html>
