#vir359
Flight: #vir359
Registration: G-VRNB
ICAO code: #4077d4
Callsign: #virgin
Operator: Virgin Atlantic
Type: AIRBUS A350-1041
Country: 🇬🇧
Speed: 838 kmh
Altitude: 11582 m
Distance: 7.1 km
Angle ∆: 58.6°
Direction ->: WNW
Track:
https://tinyurl.com/2xnmryv4
History […]
Original post on mastodon.social
mastodon.social
December 2, 2024 at 8:13 AM
Flight: #vir359
ICAO code: #4079cc
Callsign: #virgin
Operator: Virgin Atlantic
Country: 🇬🇧
From: #bom to #lhr
Speed: 693 kmh
Altitude: 11582 m
Distance: 9.0 km
Angle ∆: 52.1°
Direction ->: WNW
Track:
https://tinyurl.com/28um8nls
History:
https://www.radarbox.com/data/mode-s/4079CC
Seen: 67x
Follow Flight 4079CC from Mumbai to London Heathrow on AirNav Radar
Real-time flight tracking with one of the best and most accurate ADS-B coverage worldwide. Check airport arrivals and departures status and aircraft history.
www.airnavradar.com
January 2, 2025 at 7:51 AM
ICAO: #4072EA
Owner: #VIRGINATLANTIC
Flt: #VIR359 #GVWOO #B789 #BOM-#LHR
Time: 2026-08-13 09:15:55 CEST
Min Alt: 11575 m
Min Dist: 0.82 km (-166deg SSW)
Squawk: #2772
#adsb #planefence by kx1t sdr-e airplanes flightaware
August 13, 2026 at 7:21 AM
ICAO: #4072EA
Owner: #VIRGINATLANTIC
Flt: #VIR359 #GVWOO #B789 #BOM-#LHR
Time: 2026-08-01 09:29:02 CEST
Min Alt: 12184 m
Min Dist: 1.99 km (-144deg SW)
Squawk: #7532
#adsb #planefence by kx1t sdr-e airplanes flightaware
August 1, 2026 at 7:34 AM
ICAO: #406E10
Owner: #VIRGINATLANTIC
Flt: #VIR359 #GVDIA #B789 #BOM-#LHR
Time: 2026-06-28 09:01:23 CEST
Min Alt: 11567 m
Min Dist: 0.71 km (-160deg SSW)
Squawk: #7517
#adsb #planefence by kx1t sdr-e airplanes flightaware
June 28, 2026 at 7:06 AM
Flight: #vir359
Registration: G-VSPY
ICAO code: #406d4d
Callsign: #virgin
Operator: Virgin Atlantic
Type: BOEING 787-9
Country: 🇬🇧
From: #bom to #lhr
Speed: 744 kmh
Altitude: 11575 m
Distance: 9.2 km
Angle ∆: 51.6°
Direction ->: WNW
Track:
https://tinyurl.com/2y5rv9th
History […]
Original post on mastodon.social
mastodon.social
September 15, 2025 at 7:27 AM
Flight: #vir359
Registration: G-VNYL
ICAO code: #407371
Callsign: #virgin
Operator: Virgin Atlantic
Type: BOEING 787-9
Country: 🇬🇧
From: #bom to #lhr
Speed: 797 kmh
Altitude: 11575 m
Distance: 9.7 km
Angle ∆: 50.2°
Direction ->: WNW
Track:
https://tinyurl.com/29sut3nt
History […]
Original post on mastodon.social
mastodon.social
September 12, 2025 at 8:19 AM
Flight: #vir359
Registration: G-VNYL
ICAO code: #407371
Callsign: #virgin
Operator: Virgin Atlantic
Type: BOEING 787-9
Country: 🇬🇧
From: #bom to #lhr
Speed: 810 kmh
Altitude: 11575 m
Distance: 7.4 km
Angle ∆: 57.4°
Direction ->: WNW
Track:
https://tinyurl.com/2dqkvrr7
History […]
Original post on mastodon.social
mastodon.social
September 11, 2025 at 7:32 AM
Flight: #vir359
Registration: G-VAHH
ICAO code: #406b48
Callsign: #virgin
Operator: Virgin Atlantic
Type: BOEING 787-9
Country: 🇬🇧
From: #bom to #lhr
Speed: 895 kmh
Altitude: 12184 m
Distance: 8.7 km
Angle ∆: 54.5°
Direction ->: WNW
Track:
https://tinyurl.com/2chbg8lz
History […]
Original post on mastodon.social
mastodon.social
September 9, 2025 at 7:21 AM
Flight: #vir359
Registration: G-VSPY
ICAO code: #406d4d
Callsign: #virgin
Operator: Virgin Atlantic
Type: BOEING 787-9
Country: 🇬🇧
From: #bom to #lhr
Speed: 870 kmh
Altitude: 12192 m
Distance: 8.7 km
Angle ∆: 54.5°
Direction ->: WNW
Track:
https://tinyurl.com/2ay72zlc
History […]
Original post on mastodon.social
mastodon.social
July 28, 2025 at 6:58 AM
Flight: #vir359
Registration: G-VWHO
ICAO code: #406c39
Callsign: #virgin
Operator: Virgin Atlantic
Type: BOEING 787-9
Country: 🇬🇧
From: #bom to #lhr
Speed: 843 kmh
Altitude: 12184 m
Distance: 7.4 km
Angle ∆: 58.7°
Direction ->: WNW
Track:
https://tinyurl.com/2b6cuo9b
History […]
Original post on mastodon.social
mastodon.social
July 26, 2025 at 7:02 AM
Flight: #vir359
Registration: G-VWHO
ICAO code: #406c39
Callsign: #virgin
Operator: Virgin Atlantic
Type: BOEING 787-9
Country: 🇬🇧
From: #bom to #lhr
Speed: 872 kmh
Altitude: 11582 m
Distance: 6.4 km
Angle ∆: 60.9°
Direction ->: WNW
Track:
https://tinyurl.com/274yhwuc
History […]
Original post on mastodon.social
mastodon.social
July 25, 2025 at 7:54 AM
Flight: #vir359
Registration: G-VWHO
ICAO code: #406c39
Callsign: #virgin
Operator: Virgin Atlantic
Type: BOEING 787-9
Country: 🇬🇧
From: #bom to #lhr
Speed: 947 kmh
Altitude: 11575 m
Distance: 8.0 km
Angle ∆: 55.2°
Direction ->: WNW
Track:
https://tinyurl.com/26kfuwkc
History […]
Original post on mastodon.social
mastodon.social
July 24, 2025 at 7:19 AM
Flight: #vir359
Registration: G-VYUM
ICAO code: #406c3a
Callsign: #virgin
Operator: Virgin Atlantic
Type: BOEING 787-9
Country: 🇬🇧
From: #bom to #lhr
Speed: 808 kmh
Altitude: 12192 m
Distance: 8.5 km
Angle ∆: 55.0°
Direction ->: WNW
Track:
https://tinyurl.com/ymnkuyhv
History […]
Original post on mastodon.social
mastodon.social
July 16, 2025 at 7:10 AM
Flight: #vir359
Registration: G-VMAP
ICAO code: #406ecd
Callsign: #virgin
Operator: Virgin Atlantic
Type: BOEING 787-9
Country: 🇬🇧
From: #bom to #lhr
Speed: 843 kmh
Altitude: 12192 m
Distance: 10.1 km
Angle ∆: 50.3°
Direction ->: WNW
Track:
https://tinyurl.com/yu2pq3fb
History […]
Original post on mastodon.social
mastodon.social
July 14, 2025 at 7:15 AM
Flight: #vir359
Registration: G-VNEW
ICAO code: #406b49
Callsign: #virgin
Operator: Virgin Atlantic
Type: BOEING 787-9
Country: 🇬🇧
From: #bom to #lhr
Speed: 802 kmh
Altitude: 11582 m
Distance: 9.2 km
Angle ∆: 51.6°
Direction ->: WNW
Track:
https://tinyurl.com/2y4nfmmo
History […]
Original post on mastodon.social
mastodon.social
June 16, 2025 at 7:45 AM
Flight: #vir359
Registration: G-VBEL
ICAO code: #4073ce
Callsign: #virgin
Operator: Virgin Atlantic
Type: BOEING 787-9
Country: 🇬🇧
From: #bom to #lhr
Speed: 890 kmh
Altitude: 11575 m
Distance: 8.0 km
Angle ∆: 55.2°
Direction ->: WNW
Track:
https://tinyurl.com/28dgargl
History […]
Original post on mastodon.social
mastodon.social
June 14, 2025 at 7:34 AM
Flight: #vir359
Registration: G-VBEL
ICAO code: #4073ce
Callsign: #virgin
Operator: Virgin Atlantic
Type: BOEING 787-9
Country: 🇬🇧
From: #bom to #lhr
Speed: 879 kmh
Altitude: 11582 m
Distance: 7.7 km
Angle ∆: 56.3°
Direction ->: WNW
Track:
https://tinyurl.com/22nrr6wq
History […]
Original post on mastodon.social
mastodon.social
June 12, 2025 at 7:17 AM
Flight: #vir359
Registration: G-VYUM
ICAO code: #406c3a
Callsign: #virgin
Operator: Virgin Atlantic
Type: BOEING 787-9
Country: 🇬🇧
From: #bom to #lhr
Speed: 802 kmh
Altitude: 11582 m
Distance: 8.9 km
Angle ∆: 52.6°
Direction ->: WNW
Track:
https://tinyurl.com/275tw76p
History […]
Original post on mastodon.social
mastodon.social
June 5, 2025 at 7:09 AM
Flight: #vir359
Registration: G-VBEL
ICAO code: #4073ce
Callsign: #virgin
Operator: Virgin Atlantic
Type: BOEING 787-9
Country: 🇬🇧
From: #bom to #lhr
Speed: 812 kmh
Altitude: 12192 m
Distance: 8.4 km
Angle ∆: 55.5°
Direction ->: WNW
Track:
https://tinyurl.com/2cgrwfsd
History […]
Original post on mastodon.social
mastodon.social
May 24, 2025 at 7:19 AM
Flight: #vir359
Registration: G-VBEL
ICAO code: #4073ce
Callsign: #virgin
Operator: Virgin Atlantic
Type: BOEING 787-9
Country: 🇬🇧
From: #bom to #lhr
Speed: 850 kmh
Altitude: 11582 m
Distance: 7.1 km
Angle ∆: 58.6°
Direction ->: WNW
Track:
https://tinyurl.com/ywquwlgj
History […]
Original post on mastodon.social
mastodon.social
May 23, 2025 at 7:14 AM
Flight: #vir359
Registration: G-VZIG
ICAO code: #406c3b
Callsign: #virgin
Operator: Virgin Atlantic
Type: BOEING 787-9
Country: 🇬🇧
From: #bom to #lhr
Speed: 829 kmh
Altitude: 11575 m
Distance: 9.7 km
Angle ∆: 50.2°
Direction ->: WNW
Track:
https://tinyurl.com/yop7vl4g
History […]
Original post on mastodon.social
mastodon.social
May 20, 2025 at 7:20 AM
Flight: #vir359
Registration: G-VZIG
ICAO code: #406c3b
Callsign: #virgin
Operator: Virgin Atlantic
Type: BOEING 787-9
Country: 🇬🇧
From: #bom to #lhr
Speed: 882 kmh
Altitude: 12192 m
Distance: 7.2 km
Angle ∆: 59.3°
Direction ->: WNW
Track:
https://tinyurl.com/ysduo99f
History […]
Original post on mastodon.social
mastodon.social
May 19, 2025 at 7:04 AM
Flight: #vir359
Registration: G-VZIG
ICAO code: #406c3b
Callsign: #virgin
Operator: Virgin Atlantic
Type: BOEING 787-9
Country: 🇬🇧
From: #bom to #lhr
Altitude: 11887 m
Distance: 5.8 km
Angle ∆: 64.0°
Track:
https://tinyurl.com/yko2fc8f
History:
https://www.radarbox.com/data/mode-s/406C3B […]
Original post on mastodon.social
mastodon.social
May 18, 2025 at 7:08 AM
Flight: #vir359
ICAO code: #406e11
Callsign: #virgin
Operator: Virgin Atlantic
Country: 🇬🇧
From: #bom to #lhr
Speed: 897 kmh
Altitude: 12192 m
Distance: 8.7 km
Angle ∆: 54.5°
Direction ->: WNW
Track:




503 Service […]</div> <a href="https://mastodon.social/@RadarMechelen/114510573185924991" target="_blank" rel="noopener" class="block mt-2 border rounded overflow-hidden w-[calc(100%+3.5rem)] -ml-[3.5rem] no-card-link"> <div class="p-2"> <div class="text-sm font-semibold truncate">Original post on mastodon.social</div> <div class="text-xs text-gray-500 truncate"></div> </div> <div class="border-t px-2 py-2 text-sm text-gray-600 flex items-center gap-2"> <svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="12" r="10"/><path d="M2 12h20"/><path d="M12 2a15.3 15.3 0 0 1 0 20"/><path d="M12 2a15.3 15.3 0 0 0 0 20"/></svg> <span class="truncate">mastodon.social</span> </div> </a> <div class="mt-3 text-xs text-gray-500 flex items-center gap-2 whitespace-nowrap overflow-hidden w-[calc(100%+3.5rem)] -ml-[3.5rem]"> <span>May 15, 2025 at 7:08 AM</span> </div> <div class="border-t my-2 w-[calc(100%+3.5rem)] -ml-[3.5rem]"></div> <div class="mt-3 flex items-center justify-between text-gray-600 text-sm w-[calc(100%+3.5rem)] -ml-[3.5rem] no-card-link" data-controller="bluesky-interactions"> <div class="flex items-center justify-between w-[300px] pr-4 no-card-link"> <div class="hidden" data-post-text-uri="at://did:plc:3szm5t3tknphjtj73twqfonw/app.bsky.feed.post/3lp6x62u4vjl2"> Flight: <a href="/hashtag/vir359" class="hover:underline text-blue-600 dark:text-sky-400 no-card-link">#vir359</a><br>ICAO code: <a href="/hashtag/406e11" class="hover:underline text-blue-600 dark:text-sky-400 no-card-link">#406e11</a><br>Callsign: <a href="/hashtag/virgin" class="hover:underline text-blue-600 dark:text-sky-400 no-card-link">#virgin</a><br>Operator: Virgin Atlantic<br>Country: 🇬🇧<br>From: <a href="/hashtag/bom" class="hover:underline text-blue-600 dark:text-sky-400 no-card-link">#bom</a> to <a href="/hashtag/lhr" class="hover:underline text-blue-600 dark:text-sky-400 no-card-link">#lhr</a><br>Speed: 897 kmh<br>Altitude: 12192 m<br>Distance: 8.7 km<br>Angle ∆: 54.5°<br>Direction ->: WNW<br>Track:<br><!DOCTYPE html><br><html lang="en"><br><head><br><meta charset="UTF-8" /><br><title>503 Service […] </div> <button type="button" class="flex items-center gap-1 hover:text-black no-card-link" data-action="click->bluesky-auth#requireConnection click->bluesky-interactions#reply" data-bluesky-interactions-reply-uri-param="at://did:plc:3szm5t3tknphjtj73twqfonw/app.bsky.feed.post/3lp6x62u4vjl2" data-bluesky-interactions-reply-cid-param="bafyreidrbnpraep7ad64x6mwvwnkapvxwbxaej5wg2ysegnoqp3kwj4kma" data-bluesky-interactions-reply-root-uri-param="at://did:plc:3szm5t3tknphjtj73twqfonw/app.bsky.feed.post/3lp6x62u4vjl2" data-bluesky-interactions-reply-root-cid-param="bafyreidrbnpraep7ad64x6mwvwnkapvxwbxaej5wg2ysegnoqp3kwj4kma" data-bluesky-interactions-reply-author-name-param="📡 Flight Radar Mechelen 🇧🇪" data-bluesky-interactions-reply-author-avatar-param="https://cdn.bsky.app/img/avatar/plain/did:plc:3szm5t3tknphjtj73twqfonw/bafkreifomfnf3odha7j4v5dwsiuywgj6viqnrq2zeujvl42qvkc37xemje" data-bluesky-interactions-post-path-param="/bsky/radarmechelen.mastodon.social.ap.brid.gy/post/3lp6x62u4vjl2" title="Reply" style="pointer-events: auto; cursor: pointer; z-index: 10; position: relative;"> <svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M21 15a4 4 0 0 1-4 4H7l-4 4V7a4 4 0 0 1 4-4h10a4 4 0 0 1 4 4z"/></svg> </button> <div class="relative no-card-link" data-controller="bluesky-repost-menu" data-bluesky-repost-menu-uri-value="at://did:plc:3szm5t3tknphjtj73twqfonw/app.bsky.feed.post/3lp6x62u4vjl2" data-bluesky-repost-menu-cid-value="bafyreidrbnpraep7ad64x6mwvwnkapvxwbxaej5wg2ysegnoqp3kwj4kma" data-bluesky-repost-menu-repost-uri-value="" data-bluesky-repost-menu-is-reposted-value="false" data-bluesky-repost-menu-author-handle-value="radarmechelen.mastodon.social.ap.brid.gy" data-bluesky-repost-menu-post-text-value="Flight: #VIR359 ICAO code: #406E11 Callsign: #VIRGIN Operator: Virgin Atlantic Country: 🇬🇧 From: #BOM to #LHR Speed: 897 kmh Altitude: 12192 m Distance: 8.7 km Angle ∆: 54.5° Direction ->: WNW Track: <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <title>503 Service […]" style="pointer-events:auto; cursor:pointer;"> <button type="button" class="flex items-center gap-1 hover:text-black " data-action="click->bluesky-repost-menu#toggleMenu" data-bluesky-interactions-target="repostButton" title="Repost"> <svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><polyline points="17 1 21 5 17 9"/><path d="M3 11V9a4 4 0 0 1 4-4h14"/><polyline points="7 23 3 19 7 15"/><path d="M21 13v2a4 4 0 0 1-4 4H3"/></svg> </button> <div class="hidden absolute left-0 bottom-full mb-3 w-48 rounded-2xl border border-white/10 bg-[#1C2333] text-white shadow-xl overflow-hidden z-50" data-bluesky-repost-menu-target="menu"> <button type="button" class="w-full px-4 py-3 flex items-center gap-3 text-sm hover:bg-[#273045] text-left hidden" data-action="click->bluesky-auth#requireConnection click->bluesky-repost-menu#undoRepost"> <svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><polyline points="17 1 21 5 17 9"/><path d="M3 11V9a4 4 0 0 1 4-4h14"/><polyline points="7 23 3 19 7 15"/><path d="M21 13v2a4 4 0 0 1-4 4H3"/></svg> <span>Undo repost</span> </button> <button type="button" class="w-full px-4 py-3 flex items-center gap-3 text-sm hover:bg-[#273045] text-left " data-action="click->bluesky-auth#requireConnection click->bluesky-repost-menu#repost"> <svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><polyline points="17 1 21 5 17 9"/><path d="M3 11V9a4 4 0 0 1 4-4h14"/><polyline points="7 23 3 19 7 15"/><path d="M21 13v2a4 4 0 0 1-4 4H3"/></svg> <span>Repost</span> </button> <button type="button" class="w-full px-4 py-3 flex items-center gap-3 text-sm hover:bg-[#273045] text-left" data-action="click->bluesky-auth#requireConnection click->bluesky-repost-menu#quote"> <svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/></svg> <span>Quote post</span> </button> </div> </div> <button type="button" class="flex items-center gap-1 hover:text-black no-card-link " data-action="click->bluesky-auth#requireConnection click->bluesky-interactions#toggleLike" data-bluesky-interactions-uri-param="at://did:plc:3szm5t3tknphjtj73twqfonw/app.bsky.feed.post/3lp6x62u4vjl2" data-bluesky-interactions-cid-param="bafyreidrbnpraep7ad64x6mwvwnkapvxwbxaej5wg2ysegnoqp3kwj4kma" data-bluesky-interactions-like-uri-param="" data-bluesky-interactions-target="likeButton" title="Like"> <svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 1 0-7.78 7.78L12 21.23l8.84-8.84a5.5 5.5 0 0 0 0-7.78z"/></svg> </button> </div> <div class="flex items-center gap-4 text-gray-500"> <button type="button" class="flex items-center hover:text-black no-card-link" data-controller="share" data-share-url-value="https://www.lightnews.app/bsky/radarmechelen.mastodon.social.ap.brid.gy/post/3lp6x62u4vjl2" data-share-title-value="📡 Flight Radar Mechelen 🇧🇪 on LightNews" data-share-text-value="Flight: #VIR359 ICAO code: #406E11 Callsign: #VIRGIN Operator: Virgin Atlantic Country: 🇬🇧 From: #BOM to #LHR Speed: 897 kmh Altitude: 12192…" data-action="click->share#shareSystem" title="Share" style="pointer-events:auto; cursor:pointer;"> <svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M4 12v7a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-7"/><polyline points="16 6 12 2 8 6"/><line x1="12" y1="2" x2="12" y2="15"/></svg> </button> <div class="relative no-card-link" data-controller="bluesky-options" data-bluesky-options-text-value="Flight: #VIR359 ICAO code: #406E11 Callsign: #VIRGIN Operator: Virgin Atlantic Country: 🇬🇧 From: #BOM to #LHR Speed: 897 kmh Altitude: 12192 m Distance: 8.7 km Angle ∆: 54.5° Direction ->: WNW Track: <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <title>503 Service […]" data-bluesky-options-author-handle-value="radarmechelen.mastodon.social.ap.brid.gy" data-bluesky-options-author-did-value="did:plc:3szm5t3tknphjtj73twqfonw" data-bluesky-options-muted-value="false" data-bluesky-options-blocking-uri-value="" data-bluesky-options-own-value="false" data-bluesky-options-delete-url-value="" data-bluesky-options-post-element-selector-value='[data-card-type="bsky-post"]'> <button type="button" class="inline-flex items-center" data-action="click->bluesky-options#toggleMenu" aria-label="Post options"> <svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="12" r="1"/><circle cx="19" cy="12" r="1"/><circle cx="5" cy="12" r="1"/></svg> </button> <div class="hidden absolute right-0 bottom-full mb-3 w-60 rounded-2xl border border-white/10 bg-[#1C2333] text-white shadow-xl overflow-hidden" data-bluesky-options-target="menu"> <button type="button" class="w-full px-4 py-3 flex items-center gap-3 text-sm hover:bg-[#273045] text-left" data-action="click->bluesky-options#translate"> <span class="inline-flex items-center justify-center w-8 h-8 rounded-full bg-white/10"> <svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M4 4h16v16H4z"/><path d="M9 8h6"/><path d="M9 12h6"/><path d="M9 16h6"/></svg> </span> <span>Translate</span> </button> <button type="button" class="w-full px-4 py-3 flex items-center gap-3 text-sm hover:bg-[#273045] text-left" data-action="click->bluesky-options#copyText"> <span class="inline-flex items-center justify-center w-8 h-8 rounded-full bg-white/10"> <svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="8" y="8" width="12" height="12" rx="2"/><path d="M16 8V6a2 2 0 0 0-2-2h-8a2 2 0 0 0-2 2v8"/></svg> </span> <span>Copy post text</span> </button> <div class="border-t border-white/10"></div> <button type="button" class="w-full px-4 py-3 flex items-center gap-3 text-sm hover:bg-[#273045] text-left" data-action="click->bluesky-auth#requireConnection click->bluesky-options#muteAccount"> <span class="inline-flex items-center justify-center w-8 h-8 rounded-full bg-white/10"> <svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M4 9v6h3l5 5V4L7 9H4z"/><line x1="16" y1="9" x2="19" y2="12"/><line x1="19" y1="12" x2="16" y2="15"/></svg> </span> <span data-bluesky-options-target="muteLabel">Mute account</span> </button> <button type="button" class="w-full px-4 py-3 flex items-center gap-3 text-sm hover:bg-[#273045] text-left" data-action="click->bluesky-auth#requireConnection click->bluesky-options#blockAccount"> <span class="inline-flex items-center justify-center w-8 h-8 rounded-full bg-white/10"> <svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M3 3h18v18H3z"/><path d="M3 3l18 18"/></svg> </span> <span data-bluesky-options-target="blockLabel">Block account</span> </button> </div> </div> </div> </div> </div> </div> </div> </div> <script> // Prevent navigation when clicking in avatar column area - must run in capture phase before global handler // Only initialize once to avoid duplicate listeners if (!window._avatarColumnNoNavInitialized) { window._avatarColumnNoNavInitialized = true; document.addEventListener('click', function(ev) { try { // Check if target itself has the class OR is inside an element with that class var avatarColumn = (ev.target.classList && ev.target.classList.contains('avatar-column-no-nav')) || (ev.target.closest && ev.target.closest('.avatar-column-no-nav')); if (avatarColumn) { // Only allow navigation if clicking directly on the avatar link itself var isAvatarLink = ev.target.tagName === 'A' && ev.target.href && ev.target.href.indexOf('/author') !== -1; if (!isAvatarLink) { ev.stopPropagation(); ev.stopImmediatePropagation(); ev.preventDefault(); return false; } } } catch(e) {} }, true); // Capture phase - runs BEFORE other handlers } </script> </div> <div id="infinite-scroll-sentinel" class="mt-4" data-infinite-scroll-target="sentinel"> <a class="text-blue-600" data-action="click->infinite-scroll#onClickLoadMore" data-infinite-scroll-target="link" href="/hashtag/vir359?cursor=eyJzIjpbMCwiZGlkOnBsYzozc3ptNXQzdGtucGhqdGo3M3R3cWZvbnciLCIzbHA2eDYydTR2amwyIl0sIm8iOiIyMDI2LTEwLTA5VDE2OjEzOjI0WiJ9">Load more</a> </div> </div> </div> </div> </main> <!-- mobile footer CTA will be rendered after the grid to avoid stacking issues --> </div> <div class="hidden md:block"></div> </div> <!-- Mobile footer: CTA when logged out; mini-nav when logged in --> <div id="cta-footer" class="md:hidden fixed bottom-0 left-0 right-0 z-40 bg-white dark:bg-black border-t border-gray-200 dark:border-gray-700"> <div class="px-2 pt-3 pb-5"> <div class="flex items-center justify-between gap-3"> <!-- Left: Logo + Text --> <div class="flex items-center gap-2 flex-1 min-w-0"> <a class="inline-flex items-center gap-2" aria-label="LightNews" data-apply-edition="1" href="/"> <img alt="LightNews" class="logo-light h-12 w-12 flex-shrink-0" src="/assets/brands/lightnews/logo-light-333069a8.png" /> <img alt="LightNews" class="logo-dark h-12 w-12 flex-shrink-0" src="/assets/brands/lightnews/logo-dark-ef2a5b66.png" /> <span class="text-base font-semibold text-gray-900 dark:text-white truncate brand-text">LightNews</span> </a> </div> <!-- Right: Buttons --> <div class="flex items-center gap-2 flex-shrink-0"> <button type="button" data-action="click->auth-modal#openSignup" class="btn btn-primary btn-sm whitespace-nowrap">Create account</button> <button type="button" data-action="click->auth-modal#openSignin" class="btn btn-light btn-sm whitespace-nowrap">Sign in</button> </div> </div> </div> </div> <!-- Add bottom padding to body on mobile to prevent content from being hidden behind sticky footer --> <style> @media (max-width: 767px) { body { padding-bottom: 6.5rem; } } /* Hard guarantee: footer is visible on mobile widths */ @media (max-width: 1023px) { #cta-footer { display: block !important; } } </style> <!-- Sign In Modal --> <div id="signin-modal" class="hidden fixed inset-0 z-50 overflow-y-auto" data-auth-modal-target="signinModal"> <div class="flex min-h-screen items-center justify-center p-4"> <!-- Backdrop --> <div class="fixed inset-0 bg-black/80 transition-opacity" data-action="click->auth-modal#close"></div> <!-- Modal Content --> <div class="relative bg-white dark:bg-black rounded-2xl w-full max-w-md p-8 shadow-2xl"> <!-- Close Button --> <button data-action="click->auth-modal#close" class="absolute top-4 right-4 text-gray-400 hover:text-white transition-colors"> <svg viewBox="0 0 24 24" width="24" height="24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> <line x1="18" y1="6" x2="6" y2="18"></line> <line x1="6" y1="6" x2="18" y2="18"></line> </svg> </button> <!-- Logo --> <div class="flex justify-center mb-6"> <img alt="LightNews" class="logo-light h-12 w-12" src="/assets/brands/lightnews/logo-light-333069a8.png" /> <img alt="LightNews" class="logo-dark h-12 w-12" src="/assets/brands/lightnews/logo-dark-ef2a5b66.png" /> </div> <!-- Title --> <h2 class="text-black dark:text-white text-3xl font-bold text-center mb-6">Sign in to LightNews</h2> <!-- OAuth Buttons --> <div class="space-y-3 mb-6"> <!-- Google --> <a class="w-full flex items-center justify-center gap-3 px-6 py-3 bg-transparent hover:bg-black/5 dark:hover:bg-white/10 text-black dark:text-white font-semibold rounded transition-colors border border-gray-300 dark:border-gray-600" href="/auth/google/start"> <svg viewBox="0 0 24 24" width="20" height="20" fill="currentColor"> <path d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z" fill="#4285F4"/> <path d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z" fill="#34A853"/> <path d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z" fill="#FBBC05"/> <path d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z" fill="#EA4335"/> </svg> Continue with Google </a> <!-- Apple --> <a class="w-full flex items-center justify-center gap-3 px-6 py-3 bg-transparent hover:bg-black/5 dark:hover:bg-white/10 text-black dark:text-white font-semibold rounded transition-colors border border-gray-300 dark:border-gray-600" href="/auth/apple/start"> <svg viewBox="0 0 24 24" width="20" height="20" fill="currentColor"> <path d="M17.05 20.28c-.98.95-2.05.8-3.08.35-1.09-.46-2.09-.48-3.24 0-1.44.62-2.2.44-3.06-.35C2.79 15.25 3.51 7.59 9.05 7.31c1.35.07 2.29.74 3.08.8 1.18-.24 2.31-.93 3.57-.84 1.51.12 2.65.72 3.4 1.8-3.12 1.87-2.38 5.98.48 7.13-.57 1.5-1.31 2.99-2.54 4.09l.01-.01zM12.03 7.25c-.15-2.23 1.66-4.07 3.74-4.25.29 2.58-2.34 4.5-3.74 4.25z"/> </svg> Continue with Apple </a> <!-- Bluesky --> <button type="button" class="w-full flex items-center justify-center gap-3 px-6 py-3 bg-transparent hover:bg-black/5 dark:hover:bg-white/10 text-black dark:text-white font-semibold rounded transition-colors border border-gray-300 dark:border-gray-600" data-action="click->auth-modal#close click->bluesky-auth#handleSignIn"> <svg viewBox="0 0 24 24" width="20" height="20" fill="currentColor"> <path d="M12 10.8c-1.087-2.114-4.046-6.053-6.798-7.995C2.566.944 1.561 1.266.902 1.565.139 1.908 0 3.08 0 3.768c0 .69.378 5.65.624 6.479.815 2.736 3.713 3.66 6.383 3.364.136-.02.275-.038.415-.056-.138.022-.276.04-.415.056-3.912.58-7.387 2.005-2.83 7.078 5.013 5.19 6.87-1.113 7.823-4.308.953 3.195 2.05 9.271 7.733 4.308 4.267-4.308 1.172-6.498-2.74-7.078a8.741 8.741 0 01-.415-.056c.14.018.279.036.415.056 2.67.297 5.568-.628 6.383-3.364.246-.828.624-5.79.624-6.478 0-.69-.139-1.861-.902-2.206-.659-.298-1.664-.62-4.3 1.24C16.046 4.748 13.087 8.687 12 10.8z" fill="#1185fe"/> </svg> Continue with Bluesky </button> </div> <!-- Divider --> <div class="relative my-6"> <div class="absolute inset-0 flex items-center"> <div class="w-full border-t border-gray-300 dark:border-gray-700"></div> </div> <div class="relative flex justify-center"> <span class="bg-white dark:bg-black px-3 text-sm text-gray-600 dark:text-gray-300">or</span> </div> </div> <!-- Email Sign-In Form --> <form class="space-y-4" action="/auth/email/signin" accept-charset="UTF-8" method="post"><input type="hidden" name="authenticity_token" value="NqjSLjsFIeE3oarZABmj_4AauHCcmEQtt7P-a_ZDeKX1E3xLOF7-BiGJJHihdDS-QZGNu34vClnEHzODFnAB6A" autocomplete="off" /> <div> <label for="signin-email" class="block text-black dark:text-white font-semibold text-sm mb-2">Email address</label> <input type="email" name="email" id="signin-email" placeholder="name@domain.com" required class="w-full px-4 py-3 bg-gray-50 dark:bg-gray-900 border border-gray-300 dark:border-gray-700 rounded text-black dark:text-white placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-blue-600 focus:border-transparent" /> </div> <div> <label for="signin-password" class="block text-black dark:text-white font-semibold text-sm mb-2">Password</label> <input type="password" name="password" id="signin-password" placeholder="Your password" required class="w-full px-4 py-3 bg-gray-50 dark:bg-gray-900 border border-gray-300 dark:border-gray-700 rounded text-black dark:text-white placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-blue-600 focus:border-transparent" /> </div> <button type="submit" class="w-full btn btn-primary py-3 font-semibold"> Sign in </button> </form> <div class="mt-3 text-center"> <a class="text-sm text-blue-600 dark:text-blue-400 hover:underline" href="/auth/email/forgot_password">Forgot password?</a> </div> <!-- Footer --> <div class="mt-8 text-center"> <p class="text-gray-600 dark:text-gray-400 mb-2">Don't have an account?</p> <button data-action="click->auth-modal#switchToSignup" class="text-blue-600 dark:text-white font-semibold underline hover:text-blue-500 transition-colors"> Sign up for LightNews </button> </div> </div> </div> </div> <!-- Sign Up Modal --> <div id="signup-modal" class="hidden fixed inset-0 z-50 overflow-y-auto" data-auth-modal-target="signupModal"> <div class="flex min-h-screen items-center justify-center p-4"> <!-- Backdrop --> <div class="fixed inset-0 bg-black/80 transition-opacity" data-action="click->auth-modal#close"></div> <!-- Modal Content --> <div class="relative bg-white dark:bg-black rounded-2xl w-full max-w-md p-8 shadow-2xl"> <!-- Close Button --> <button data-action="click->auth-modal#close" class="absolute top-4 right-4 text-gray-400 hover:text-white transition-colors"> <svg viewBox="0 0 24 24" width="24" height="24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> <line x1="18" y1="6" x2="6" y2="18"></line> <line x1="6" y1="6" x2="18" y2="18"></line> </svg> </button> <!-- Logo --> <div class="flex justify-center mb-6"> <img alt="LightNews" class="logo-light h-12 w-12" src="/assets/brands/lightnews/logo-light-333069a8.png" /> <img alt="LightNews" class="logo-dark h-12 w-12" src="/assets/brands/lightnews/logo-dark-ef2a5b66.png" /> </div> <!-- Title --> <h2 class="text-black dark:text-white text-3xl font-bold text-center mb-6">Sign up to start reading</h2> <!-- OAuth Buttons --> <div class="space-y-3 mb-6"> <!-- Google --> <a class="w-full flex items-center justify-center gap-3 px-6 py-3 bg-transparent hover:bg-black/5 dark:hover:bg-white/10 text-black dark:text-white font-semibold rounded transition-colors border border-gray-300 dark:border-gray-600" href="/auth/google/start"> <svg viewBox="0 0 24 24" width="20" height="20" fill="currentColor"> <path d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z" fill="#4285F4"/> <path d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z" fill="#34A853"/> <path d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z" fill="#FBBC05"/> <path d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z" fill="#EA4335"/> </svg> Sign up with Google </a> <!-- Apple --> <a class="w-full flex items-center justify-center gap-3 px-6 py-3 bg-transparent hover:bg-black/5 dark:hover:bg-white/10 text-black dark:text-white font-semibold rounded transition-colors border border-gray-300 dark:border-gray-600" href="/auth/apple/start"> <svg viewBox="0 0 24 24" width="20" height="20" fill="currentColor"> <path d="M17.05 20.28c-.98.95-2.05.8-3.08.35-1.09-.46-2.09-.48-3.24 0-1.44.62-2.2.44-3.06-.35C2.79 15.25 3.51 7.59 9.05 7.31c1.35.07 2.29.74 3.08.8 1.18-.24 2.31-.93 3.57-.84 1.51.12 2.65.72 3.4 1.8-3.12 1.87-2.38 5.98.48 7.13-.57 1.5-1.31 2.99-2.54 4.09l.01-.01zM12.03 7.25c-.15-2.23 1.66-4.07 3.74-4.25.29 2.58-2.34 4.5-3.74 4.25z"/> </svg> Sign up with Apple </a> <!-- Bluesky --> <button type="button" class="w-full flex items-center justify-center gap-3 px-6 py-3 bg-transparent hover:bg-black/5 dark:hover:bg-white/10 text-black dark:text-white font-semibold rounded transition-colors border border-gray-300 dark:border-gray-600" data-action="click->auth-modal#close click->bluesky-auth#handleCreateAccount"> <svg viewBox="0 0 24 24" width="20" height="20" fill="currentColor"> <path d="M12 10.8c-1.087-2.114-4.046-6.053-6.798-7.995C2.566.944 1.561 1.266.902 1.565.139 1.908 0 3.08 0 3.768c0 .69.378 5.65.624 6.479.815 2.736 3.713 3.66 6.383 3.364.136-.02.275-.038.415-.056-.138.022-.276.04-.415.056-3.912.58-7.387 2.005-2.83 7.078 5.013 5.19 6.87-1.113 7.823-4.308.953 3.195 2.05 9.271 7.733 4.308 4.267-4.308 1.172-6.498-2.74-7.078a8.741 8.741 0 01-.415-.056c.14.018.279.036.415.056 2.67.297 5.568-.628 6.383-3.364.246-.828.624-5.79.624-6.478 0-.69-.139-1.861-.902-2.206-.659-.298-1.664-.62-4.3 1.24C16.046 4.748 13.087 8.687 12 10.8z" fill="#1185fe"/> </svg> Sign up with Bluesky </button> </div> <!-- Divider --> <div class="relative my-6"> <div class="absolute inset-0 flex items-center"> <div class="w-full border-t border-gray-300 dark:border-gray-700"></div> </div> <div class="relative flex justify-center"> <span class="bg-white dark:bg-black px-3 text-sm text-gray-600 dark:text-gray-300">or</span> </div> </div> <!-- Email Sign-Up Form --> <form class="space-y-4" action="/auth/email/signup" accept-charset="UTF-8" method="post"><input type="hidden" name="authenticity_token" value="gctMW-8iolEum1hE12f88AcQgqQs2U2RS1MHpVOauA0jGLdEZERdeBfmw2PbKYqqTlhxgCgzBQ30qCJ-W3Tlzw" autocomplete="off" /> <div> <label for="signup-name" class="block text-black dark:text-white font-semibold text-sm mb-2">Name</label> <input type="text" name="display_name" id="signup-name" placeholder="Your name" required class="w-full px-4 py-3 bg-gray-50 dark:bg-gray-900 border border-gray-300 dark:border-gray-700 rounded text-black dark:text-white placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-blue-600 focus:border-transparent" /> </div> <div> <label for="signup-email" class="block text-black dark:text-white font-semibold text-sm mb-2">Email address</label> <input type="email" name="email" id="signup-email" placeholder="name@domain.com" required class="w-full px-4 py-3 bg-gray-50 dark:bg-gray-900 border border-gray-300 dark:border-gray-700 rounded text-black dark:text-white placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-blue-600 focus:border-transparent" /> </div> <div> <label for="signup-password" class="block text-black dark:text-white font-semibold text-sm mb-2">Password</label> <input type="password" name="password" id="signup-password" placeholder="At least 8 characters" minlength="8" required class="w-full px-4 py-3 bg-gray-50 dark:bg-gray-900 border border-gray-300 dark:border-gray-700 rounded text-black dark:text-white placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-blue-600 focus:border-transparent" /> </div> <div> <label for="signup-password-confirmation" class="block text-black dark:text-white font-semibold text-sm mb-2">Confirm password</label> <input type="password" name="password_confirmation" id="signup-password-confirmation" placeholder="Repeat your password" minlength="8" required class="w-full px-4 py-3 bg-gray-50 dark:bg-gray-900 border border-gray-300 dark:border-gray-700 rounded text-black dark:text-white placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-blue-600 focus:border-transparent" /> </div> <button type="submit" class="w-full btn btn-primary py-3 font-semibold"> Create account </button> </form> <!-- Footer --> <div class="mt-8 text-center"> <p class="text-gray-600 dark:text-gray-400 mb-2">Already have an account?</p> <button data-action="click->auth-modal#switchToSignin" class="text-blue-600 dark:text-white font-semibold underline hover:text-blue-500 transition-colors"> Sign in </button> </div> </div> </div> </div> <div data-controller="modal" data-reply-modal="true" id="global-reply-modal" data-viewer-avatar-url="" data-viewer-display-name=""> <div class="hidden fixed inset-0 z-50 overflow-y-auto" data-modal-target="panel" data-viewer-avatar-url="" data-viewer-display-name=""> <div class="flex min-h-screen items-start justify-center pt-4 md:pt-20"> <!-- Backdrop --> <div class="fixed inset-0 bg-black/80 transition-opacity" data-action="click->modal#close"></div> <!-- Modal Content --> <div class="relative bg-[#1e3a5f] rounded-2xl w-full max-w-xl mx-4 shadow-2xl" data-action="click->modal#stop"> <!-- Top bar: Cancel + Reply button --> <div class="flex items-center justify-between px-4 py-3 border-b border-gray-600"> <button type="button" class="text-blue-400 text-base font-medium" data-action="click->modal#close">Cancel</button> <button type="button" id="modal-reply-submit" class="px-5 py-1.5 bg-blue-600 text-white rounded-full font-semibold hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed"> Reply </button> </div> <!-- Parent post preview (for replies) --> <div class="px-4 py-3 border-b border-gray-600 modal-reply-preview"> <div class="flex items-start gap-3"> <img src="" alt="" class="modal-reply-author-avatar w-10 h-10 rounded-full flex-shrink-0 hidden"> <div class="modal-reply-author-avatar-placeholder w-10 h-10 rounded-full bg-gray-300 flex-shrink-0"></div> <div class="flex-1 min-w-0"> <div class="font-semibold text-white text-base modal-reply-author-name"> </div> <div class="text-gray-300 text-sm line-clamp-3 modal-reply-post-text"> </div> </div> </div> </div> <!-- Reply/Post textarea --> <div class="px-4 py-3"> <div class="flex items-start gap-3"> <img src="" alt="You" class="modal-viewer-avatar w-10 h-10 rounded-full flex-shrink-0 hidden"> <div class="modal-viewer-avatar-placeholder w-10 h-10 rounded-full bg-gray-300 flex-shrink-0 "></div> <div class="flex-1 min-w-0"> <textarea id="modal-reply-text" placeholder="Write your reply" rows="8" maxlength="300" class="w-full px-0 py-2 bg-transparent text-white text-base placeholder-gray-400 resize-none focus:outline-none border-0" data-reply-uri="" data-reply-cid="" data-root-uri="" data-root-cid="" data-mode="reply" data-embed="" ></textarea> </div> </div> <!-- Quote preview (for quote posts, shown below textarea) --> <div class="modal-quote-preview hidden mt-3 p-3 border border-gray-600 rounded-xl bg-[#152642]"> <div class="flex items-start gap-3"> <img src="" alt="" class="modal-quote-author-avatar w-8 h-8 rounded-full flex-shrink-0 hidden"> <div class="modal-quote-author-avatar-placeholder w-8 h-8 rounded-full bg-gray-400 flex-shrink-0"></div> <div class="flex-1 min-w-0"> <div class="font-semibold text-white text-sm modal-quote-author-name"></div> <div class="text-gray-300 text-sm mt-1 modal-quote-post-text"></div> </div> </div> </div> <input type="file" id="modal-attachment-input" accept="image/*" class="hidden" multiple> <div id="modal-attachment-preview" class="mt-3 hidden"> <div class="flex flex-wrap gap-3" id="modal-attachment-preview-list"></div> </div> </div> <!-- Bottom toolbar --> <div class="px-4 py-3 border-t border-gray-600 flex items-center justify-between"> <div class="flex items-center gap-3"> <button type="button" class="text-blue-500 hover:text-blue-400" id="modal-attachment-button"> <svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="18" height="18" rx="2" ry="2"/><circle cx="8.5" cy="8.5" r="1.5"/><polyline points="21 15 16 10 5 21"/></svg> </button> <button type="button" class="text-blue-500 hover:text-blue-400" id="modal-gif-button"> <svg viewBox="0 0 24 24" width="20" height="20" fill="currentColor"><text x="3" y="18" font-family="Arial" font-size="14" font-weight="bold">GIF</text></svg> </button> </div> <div class="flex items-center gap-3 text-sm"> <span class="text-blue-400">English</span> <span class="text-gray-400" id="modal-reply-count">300</span> </div> </div> </div> </div> </div> </div> <script> (function(){ function setupReplyModal(){ var textarea = document.getElementById('modal-reply-text'); var submitBtn = document.getElementById('modal-reply-submit'); var countEl = document.getElementById('modal-reply-count'); var attachmentButton = document.getElementById('modal-attachment-button'); var gifButton = document.getElementById('modal-gif-button'); var attachmentInput = document.getElementById('modal-attachment-input'); var attachmentPreview = document.getElementById('modal-attachment-preview'); var attachmentList = document.getElementById('modal-attachment-preview-list'); if (!textarea || !submitBtn || !countEl) { return; } if (textarea.dataset.replyModalBound === '1') { return; } textarea.dataset.replyModalBound = '1'; var attachments = []; var currentEmbed = null; var isUploadingAttachment = false; var MAX_ATTACHMENTS = 4; function updateCount(){ var length = textarea.value.length; var remaining = 300 - length; countEl.textContent = remaining.toString(); countEl.classList.toggle('text-red-400', remaining < 0); countEl.classList.toggle('text-gray-400', remaining >= 0); submitBtn.disabled = ((length === 0) && attachments.length === 0) || length > 300; } function updateEmbedPayload() { if (!attachments.length) { currentEmbed = null; textarea.removeAttribute('data-embed'); return; } currentEmbed = { "$type": "app.bsky.embed.images", "images": attachments.map(function(item){ return { "alt": item.alt || 'Image', "image": item.blob }; }) }; textarea.dataset.embed = JSON.stringify(currentEmbed); } function refreshAttachmentPreview() { if (!attachmentList || !attachmentPreview) { return; } attachmentList.innerHTML = ''; attachments.forEach(function(item, index){ var wrapper = document.createElement('div'); wrapper.className = 'relative w-20 h-20 rounded-xl overflow-hidden'; var img = document.createElement('img'); img.src = item.previewUrl; img.alt = item.alt || 'Attachment'; img.className = 'object-cover w-full h-full'; wrapper.appendChild(img); var removeBtn = document.createElement('button'); removeBtn.type = 'button'; removeBtn.className = 'absolute top-1 right-1 bg-black/70 text-white rounded-full w-6 h-6 flex items-center justify-center text-xs'; removeBtn.innerHTML = '×'; removeBtn.addEventListener('click', function(){ removeAttachment(index); }); wrapper.appendChild(removeBtn); attachmentList.appendChild(wrapper); }); if (attachments.length) { attachmentPreview.classList.remove('hidden'); } else { attachmentPreview.classList.add('hidden'); } if (attachmentButton) { attachmentButton.disabled = attachments.length >= MAX_ATTACHMENTS || isUploadingAttachment; attachmentButton.classList.toggle('opacity-50', attachmentButton.disabled); attachmentButton.classList.toggle('pointer-events-none', attachmentButton.disabled); } if (gifButton) { gifButton.disabled = attachments.length >= MAX_ATTACHMENTS || isUploadingAttachment; gifButton.classList.toggle('opacity-50', gifButton.disabled); gifButton.classList.toggle('pointer-events-none', gifButton.disabled); } updateCount(); } function removeAttachment(index) { if (index < 0 || index >= attachments.length) { return; } var removed = attachments.splice(index, 1)[0]; if (removed && removed.previewUrl) { try { URL.revokeObjectURL(removed.previewUrl); } catch (_) {} } updateEmbedPayload(); refreshAttachmentPreview(); } function setAttachmentUploading(state) { isUploadingAttachment = state; refreshAttachmentPreview(); } async function uploadFile(file) { if (!file || !attachmentInput) { return; } if (!file.type.match(/^image\//)) { alert('Only image uploads are supported right now.'); return; } if (attachments.length >= MAX_ATTACHMENTS) { alert('You can attach up to ' + MAX_ATTACHMENTS + ' images.'); return; } if (file.size > 976 * 1024) { alert('Images must be 976KB or smaller.'); return; } setAttachmentUploading(true); var formData = new FormData(); formData.append('file', file); formData.append('alt', file.name || 'Image'); try { var response = await fetch('/api/bluesky/uploads', { method: 'POST', headers: { 'X-CSRF-Token': (document.querySelector('meta[name="csrf-token"]') || {}).content || '', 'Accept': 'application/json' }, credentials: 'same-origin', body: formData }); if (!response.ok) { var errorText = await response.text(); var message = 'Upload failed'; try { var parsed = JSON.parse(errorText); message = parsed.error || message; } catch (_) { if (errorText && errorText.length < 200) { message = errorText; } } throw new Error(message); } var data = await response.json(); if (!data || !data.success || !data.blob) { throw new Error((data && data.error) || 'Upload failed'); } var previewUrl = URL.createObjectURL(file); attachments.push({ blob: data.blob, alt: data.alt || file.name || 'Image', previewUrl: previewUrl }); updateEmbedPayload(); refreshAttachmentPreview(); } catch (error) { console.error('Attachment upload error:', error); alert(error && error.message ? error.message : 'Unable to upload image right now.'); } finally { setAttachmentUploading(false); if (attachmentInput) { attachmentInput.value = ''; } } } function handleFileSelection(files) { if (!files || !files.length) { return; } var remaining = MAX_ATTACHMENTS - attachments.length; Array.from(files).slice(0, remaining).forEach(function(file){ uploadFile(file); }); } if (attachmentButton && attachmentInput) { attachmentButton.addEventListener('click', function(e){ e.preventDefault(); if (isUploadingAttachment || attachments.length >= MAX_ATTACHMENTS) { return; } attachmentInput.setAttribute('accept', 'image/*'); attachmentInput.click(); }); } if (gifButton && attachmentInput) { gifButton.addEventListener('click', function(e){ e.preventDefault(); if (isUploadingAttachment || attachments.length >= MAX_ATTACHMENTS) { return; } attachmentInput.setAttribute('accept', 'image/gif'); attachmentInput.click(); }); } if (attachmentInput) { attachmentInput.addEventListener('change', function(e){ handleFileSelection(e.target.files); }); } refreshAttachmentPreview(); textarea.addEventListener('input', updateCount); updateCount(); submitBtn.addEventListener('click', async function(e){ e.preventDefault(); var text = textarea.value.trim(); if ((text.length === 0 && attachments.length === 0) || text.length > 300) { return; } var replyUri = textarea.dataset.replyUri; var replyCid = textarea.dataset.replyCid; var rootUri = textarea.dataset.rootUri; var rootCid = textarea.dataset.rootCid; var mode = textarea.dataset.mode || 'reply'; if (mode === 'reply' && (!replyUri || !replyCid)) { return; } submitBtn.disabled = true; var originalText = submitBtn.textContent; submitBtn.textContent = mode === 'post' ? 'Posting...' : 'Replying...'; try { var endpoint, payload; if (mode === 'post') { endpoint = '/api/bluesky/posts'; payload = { text: text }; } else { endpoint = '/api/bluesky/replies'; payload = { text: text, reply_to_uri: replyUri, reply_to_cid: replyCid, root_uri: rootUri, root_cid: rootCid }; } // Attach quote embed if present (for quote posts) - check first var quoteEmbed = textarea.dataset.quoteEmbed; if (quoteEmbed && mode === 'post' && !currentEmbed) { try { payload.embed = JSON.parse(quoteEmbed); console.log('Added quote embed:', payload.embed); } catch (e) { console.error('Failed to parse quote embed', e); } } // Attach image embed if present (overrides quote for now) if (currentEmbed) { payload.embed = currentEmbed; console.log('Added image embed:', payload.embed); } console.log('Submitting payload:', JSON.stringify(payload).substring(0, 300)); var response = await fetch(endpoint, { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': (document.querySelector('meta[name="csrf-token"]') || {}).content || '' }, credentials: 'same-origin', body: JSON.stringify(payload) }); console.log('Reply response status:', response.status); if (!response.ok) { var errorText = await response.text(); var errorMessage = 'Network error. Please try again.'; var settingsUrl = null; try { var errorData = JSON.parse(errorText); errorMessage = errorData.error || errorMessage; settingsUrl = errorData.settings_url || null; } catch (e2) { if (errorText && errorText.length < 200) { errorMessage = errorText; } else { errorMessage = 'HTTP ' + response.status + ': ' + response.statusText; } } console.error('Error response:', response.status, errorMessage); if (response.status === 401 && settingsUrl) { if (confirm(errorMessage + '\n\nWould you like to go to Settings now?')) { window.location.href = settingsUrl; return; } } else { alert(errorMessage); } submitBtn.disabled = false; submitBtn.textContent = originalText; return; } var data = await response.json(); console.log('Reply response data:', data); if (data.success) { window.location.reload(); } else { alert(data.error || (mode === 'post' ? 'Failed to create post' : 'Failed to post reply')); submitBtn.disabled = false; submitBtn.textContent = originalText; } } catch (error) { console.error('Reply error:', error); alert(error && error.message ? error.message : 'Network error. Please try again.'); submitBtn.disabled = false; submitBtn.textContent = originalText; } }); } function openReplyIfRequested(){ try{ var qs = new URLSearchParams(window.location.search); if(qs.get('reply') === '1'){ var panel = document.querySelector('[data-modal-target="panel"]'); if(panel){ panel.classList.remove('hidden'); document.documentElement.classList.add('overflow-hidden'); document.body.classList.add('overflow-hidden'); var ta = document.getElementById('modal-reply-text'); if(ta){ ta.focus(); } } } }catch(e){} } function initReplyModal(){ setupReplyModal(); openReplyIfRequested(); } document.addEventListener('turbo:load', function(){ setTimeout(initReplyModal, 0); }); if (document.readyState === 'complete' || document.readyState === 'interactive') { setTimeout(initReplyModal, 0); } else { document.addEventListener('DOMContentLoaded', initReplyModal); } })(); </script> <script> // Sanitize any legacy nav=1 param from links and the current URL document.addEventListener('click', function(ev){ try{ var a = ev.target.closest && ev.target.closest('a'); if(!a) return; var url = new URL(a.href, location.origin); if(url.searchParams.has('nav')){ url.searchParams.delete('nav'); a.setAttribute('href', url.pathname + (url.searchParams.toString() ? ('?' + url.searchParams.toString()) : '') + url.hash); } }catch(e){} }, true); document.addEventListener('turbo:load', function(){ try{ var url = new URL(location.href); if(url.searchParams.has('nav')){ url.searchParams.delete('nav'); var cleaned = url.pathname + (url.searchParams.toString() ? ('?' + url.searchParams.toString()) : '') + url.hash; history.replaceState(null, '', cleaned); } }catch(e){} }); </script> <script> function loadTwitterEmbeds() { try { if (window.twttr && window.twttr.widgets) { window.twttr.widgets.load(); return true; } } catch(e) {} return false; } document.addEventListener("turbo:load", function() { if (!loadTwitterEmbeds()) { var tries = 0; var max = 10; (function retry(){ if (loadTwitterEmbeds() || ++tries > max) return; setTimeout(retry, 400); })(); } // Re-hydrate Bluesky embeds after Turbo navigation by reinjecting the script try { var s = document.createElement("script"); s.src = "https://embed.bsky.app/static/embed.js?ts=" + Date.now(); s.async = true; s.onload = function(){ try { s.remove(); } catch(_){} }; document.body.appendChild(s); } catch (e) {} }); </script> <script> // Decorate links to lists (home/sections) with stored edition at click time (web only) document.addEventListener('click', function(ev){ try{ var a = ev.target.closest && ev.target.closest('a[data-apply-edition="1"]'); if(!a) return; var pref = localStorage.getItem('preferredEdition'); if(pref && pref !== 'global'){ var url = new URL(a.href, location.origin); url.searchParams.set('edition', pref); if(pref === 'eu'){ url.searchParams.set('signal_scope', 'expanded'); } else { url.searchParams.delete('signal_scope'); } if (url.toString() !== a.href) { // Update href and let the browser/Turbo handle the navigation once a.setAttribute('href', url.pathname + '?' + url.searchParams.toString() + url.hash); } } else { var url2 = new URL(a.href, location.origin); if(url2.searchParams.has('edition') || url2.searchParams.has('signal_scope')){ url2.searchParams.delete('edition'); url2.searchParams.delete('signal_scope'); var newHref = url2.pathname + (url2.searchParams.toString() ? ('?' + url2.searchParams.toString()) : '') + url2.hash; a.setAttribute('href', newHref); } } }catch(e){} }, true); </script> <script> // Track last list path (home or sections) for back navigation on digest pages document.addEventListener('turbo:load', function(){ try{ var path = location.pathname; if(path === '/' || path.indexOf('/sections/') === 0){ var url = new URL(location.href); var stored = path + (url.searchParams.toString() ? ('?' + url.searchParams.toString()) : ''); sessionStorage.setItem('lastListPath', stored); sessionStorage.setItem('lastNonPost', stored); } else if(path === '/trending'){ sessionStorage.setItem('lastTrendingPath', '/trending'); sessionStorage.setItem('lastNonPost', '/trending'); } else if(path.indexOf('/digests/') === 0){ // Keep lastDigestPath fresh when on digest pages to avoid stale fallbacks sessionStorage.setItem('lastDigestPath', path + location.search); sessionStorage.setItem('lastNonPost', path + location.search); } }catch(e){} }); </script> <script> // Remove any l=... language parameter from digest links to unify cache keys document.addEventListener('click', function(ev){ try{ var a = ev.target.closest && ev.target.closest('a[href^="/digests/"]'); if(!a) return; var url = new URL(a.href, location.origin); if(url.searchParams.has('l')){ url.searchParams.delete('l'); var newHref = url.pathname + (url.searchParams.toString() ? ('?' + url.searchParams.toString()) : '') + url.hash; a.setAttribute('href', newHref); } }catch(e){} }, true); </script> <script> // Remember previous page before navigating to a local Bluesky post via anchor document.addEventListener('click', function(ev){ try{ var a = ev.target.closest && ev.target.closest('a[href^="/bsky/"]'); if(!a) return; var prev = location.pathname + location.search; sessionStorage.setItem('prevPage', prev); sessionStorage.setItem('lastPostPath', location.pathname); }catch(e){} }, true); </script> <script> // Remember previous page before navigating to an author profile document.addEventListener('click', function(ev){ try{ // Match both /author and /author/... and /author?... var a = ev.target.closest && (ev.target.closest('a[href^="/author/"]') || ev.target.closest('a[href^="/author"]')); if(!a) return; var prev = location.pathname + location.search; sessionStorage.setItem('prevPage', prev); if(location.pathname.indexOf('/author/') !== 0){ sessionStorage.setItem('lastNonAuthor', prev); } sessionStorage.setItem('entrySource','author'); }catch(e){} }, true); </script> <script> // Also sanitize card-clickable data-href: strip l=... if present document.addEventListener('click', function(ev){ try{ var card = ev.target.closest && ev.target.closest('.card-clickable'); if(!card) return; var href = (card.getAttribute('data-href') || '').trim(); if(!href) return; // Strip accidental wrapping quotes if((href[0]==='"' && href[href.length-1]==='"') || (href[0]==="'" && href[href.length-1]==="'")){ href = href.slice(1,-1); } var url = new URL(href, location.origin); if(url.searchParams.has('l')){ url.searchParams.delete('l'); var newHref = url.pathname + (url.searchParams.toString() ? ('?' + url.searchParams.toString()) : '') + url.hash; card.setAttribute('data-href', newHref); } // Navigate on card click unless clicking an inner actionable link var isInlineVideo = ev.target.closest && ev.target.closest('.inline-video'); // Tag nested-card navigations so back button can return to previous post instead of thread parent var inNested = ev.target.closest && ev.target.closest('.bsky-nested, .nested-bsky, .bluesky-nested'); // Don't navigate if clicking in avatar column area (left side of post with avatar and thread lines) // Check if target itself has the class OR is inside an element with that class var isAvatarColumn = (ev.target.classList && ev.target.classList.contains('avatar-column-no-nav')) || (ev.target.closest && ev.target.closest('.avatar-column-no-nav')); if(!ev.target.closest('.no-card-link') && !isInlineVideo && !isAvatarColumn){ ev.preventDefault(); var targetHref = (card.getAttribute('data-href') || href).trim(); if((targetHref[0]==='"' && targetHref[targetHref.length-1]==='"') || (targetHref[0]==="'" && targetHref[targetHref.length-1]==="'")){ targetHref = targetHref.slice(1,-1); } try{ // If navigating from a digest page to a post, remember this digest for back navigation if(location.pathname.indexOf('/digests/') === 0){ sessionStorage.setItem('lastDigestPath', location.pathname + location.search); sessionStorage.setItem('entrySource','digest'); } else if(location.pathname.indexOf('/bsky/') === 0){ // Navigating from a post to another post (e.g., reply): mark source as post sessionStorage.setItem('entrySource', inNested ? 'nested' : 'post'); } // Always remember the immediate previous page when going to a post if(targetHref.indexOf('/bsky/') === 0){ var prevp = location.pathname + location.search; sessionStorage.setItem('prevPage', prevp); sessionStorage.setItem('lastPostPath', location.pathname); } }catch(e){} if(window.Turbo && Turbo.visit){ Turbo.visit(targetHref); } else { window.location.href = targetHref; } } }catch(e){} }, true); </script> <script> // Keyboard activation for card-clickable elements document.addEventListener('keydown', function(ev){ try{ if(ev.key !== 'Enter' && ev.key !== ' ') return; var card = ev.target.closest && ev.target.closest('.card-clickable'); if(!card) return; var isInlineVideo = ev.target.closest && ev.target.closest('.inline-video'); if(isInlineVideo) return; var href = (card.getAttribute('data-href') || '').trim(); if(!href) return; if((href[0]==='"' && href[href.length-1]==='"') || (href[0]==="'" && href[href.length-1]==="'")){ href = href.slice(1,-1); } ev.preventDefault(); if(window.Turbo && Turbo.visit){ Turbo.visit(href); } else { window.location.href = href; } }catch(e){} }, true); </script> <script> // Sync stored edition from current URL on list pages (no navigation) // Only update when the URL explicitly includes an edition param; otherwise keep prior preference. document.addEventListener("turbo:load", function(){ try{ var onDigest = location.pathname.indexOf('/digests/') === 0; if(onDigest) return; var url = new URL(location.href); if(url.searchParams.has('edition')){ var ed = url.searchParams.get('edition'); localStorage.setItem('preferredEdition', (ed && ed !== 'global') ? ed : 'global'); } else if(url.searchParams.has('signal_scope')){ localStorage.setItem('preferredEdition', 'global'); } }catch(e){} }); </script> <div data-controller="modal" data-bluesky-auth-target="modal" id="bluesky-pds-modal"> <div class="hidden fixed inset-0 z-50 overflow-y-auto" data-modal-target="panel"> <div class="flex min-h-screen items-center justify-center p-4"> <div class="fixed inset-0 bg-black/80 transition-opacity" data-action="click->modal#close"></div> <div class="relative bg-white dark:bg-black rounded-2xl w-full max-w-md p-8 shadow-2xl" data-action="click->modal#stop"> <button type="button" class="absolute top-4 right-4 text-gray-400 hover:text-black dark:hover:text-white transition" data-action="click->modal#close"> <svg viewBox="0 0 24 24" width="24" height="24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> <line x1="18" y1="6" x2="6" y2="18"></line> <line x1="6" y1="6" x2="18" y2="18"></line> </svg> </button> <div class="flex justify-center mb-6"> <img alt="LightNews" class="logo-light h-12 w-12" src="/assets/brands/lightnews/logo-light-333069a8.png" /> <img alt="LightNews" class="logo-dark h-12 w-12" src="/assets/brands/lightnews/logo-dark-ef2a5b66.png" /> </div> <h2 class="text-2xl font-bold text-center text-black dark:text-white mb-3">Connect Bluesky</h2> <p class="text-center text-sm text-gray-600 dark:text-gray-300 mb-6"> Enter your Bluesky handle and app password to unlock posting, likes, and your Following feed. </p> <div class="mb-4 hidden text-sm text-red-500" data-bluesky-auth-target="error"></div> <form data-bluesky-auth-target="form" class="space-y-4" action="/auth/bluesky/pds_session" accept-charset="UTF-8" method="post"><input type="hidden" name="authenticity_token" value="taRG1ZT-V-Mg4DdVIKg2Sds0eeHy5MfDJcPoQxHiPjCIrcj-5B7aWfleoLqRmPhqNWJ8Jm50NJ1UEmHl2raQUw" autocomplete="off" /> <div> <label class="block text-sm font-semibold text-black dark:text-white mb-2" for="identifier">Handle or email</label> <input placeholder="@you.bsky.social" autocomplete="username" required="required" class="w-full px-4 py-3 bg-gray-50 dark:bg-gray-900 border border-gray-300 dark:border-gray-700 rounded-lg text-black dark:text-white placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-blue-600 focus:border-transparent" data-bluesky-auth-target="identifierInput" type="text" name="identifier" id="identifier" /> </div> <div> <label class="block text-sm font-semibold text-black dark:text-white mb-2" for="password">App password</label> <input placeholder="xxxx-xxxx-xxxx-xxxx" autocomplete="current-password" required="required" class="w-full px-4 py-3 bg-gray-50 dark:bg-gray-900 border border-gray-300 dark:border-gray-700 rounded-lg text-black dark:text-white placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-blue-600 focus:border-transparent" data-bluesky-auth-target="passwordInput" type="password" name="password" id="password" /> </div> <div class="hidden" data-bluesky-auth-target="tfaField"> <label class="block text-sm font-semibold text-black dark:text-white mb-2" for="auth_factor_token">2FA Code</label> <input placeholder="123456" autocomplete="one-time-code" inputmode="numeric" pattern="[0-9]*" class="w-full px-4 py-3 bg-gray-50 dark:bg-gray-900 border border-gray-300 dark:border-gray-700 rounded-lg text-black dark:text-white placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-blue-600 focus:border-transparent" data-bluesky-auth-target="tfaInput" type="text" name="auth_factor_token" id="auth_factor_token" /> </div> <button type="submit" class="w-full btn btn-primary py-3 font-semibold" data-bluesky-auth-target="submitButton"> Connect Bluesky </button> </form> <p class="mt-5 text-xs text-center text-gray-500 dark:text-gray-400"> Need an app password? Open Bluesky, go to Settings > App passwords, and create a new one. </p> </div> </div> </div> </div> <!-- Bluesky OAuth Modal - Shows when logged-out user tries Bluesky action --> <div id="bluesky-oauth-modal" class="hidden fixed inset-0 z-50 overflow-y-auto" data-bluesky-auth-target="oauthModal"> <div class="flex min-h-screen items-center justify-center p-4"> <!-- Backdrop --> <div class="fixed inset-0 bg-black/80 transition-opacity" data-action="click->bluesky-auth#closeOAuthModal"></div> <!-- Modal Content --> <div class="relative bg-white dark:bg-black rounded-2xl w-full max-w-md p-8 shadow-2xl" data-action="click->bluesky-auth#stopPropagation"> <!-- Close Button --> <button data-action="click->bluesky-auth#closeOAuthModal" class="absolute top-4 right-4 text-gray-400 hover:text-white transition-colors"> <svg viewBox="0 0 24 24" width="24" height="24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> <line x1="18" y1="6" x2="6" y2="18"></line> <line x1="6" y1="6" x2="18" y2="18"></line> </svg> </button> <!-- Logo --> <div class="flex justify-center mb-6"> <img alt="LightNews" class="logo-light h-12 w-12" src="/assets/brands/lightnews/logo-light-333069a8.png" /> <img alt="LightNews" class="logo-dark h-12 w-12" src="/assets/brands/lightnews/logo-dark-ef2a5b66.png" /> </div> <!-- Title --> <h2 class="text-black dark:text-white text-3xl font-bold text-center mb-3">Connect with Bluesky</h2> <!-- Description --> <p class="text-center text-base text-gray-600 dark:text-gray-300 mb-6"> Sign in with your Bluesky account to unlock posting, likes, and your Following feed. </p> <!-- Buttons --> <div class="space-y-3"> <!-- Sign In Button - Grey --> <button type="button" data-action="click->bluesky-auth#handleSignIn" data-bluesky-auth-target="signinButton" class="bsky-signin-btn w-full flex items-center justify-center gap-3 px-6 py-3 bg-gray-600 hover:bg-gray-700 font-semibold rounded transition-colors border border-gray-600 text-white"> <svg viewBox="0 0 24 24" width="20" height="20" fill="currentColor" style="color: #1185fe !important;"> <path d="M12 10.8c-1.087-2.114-4.046-6.053-6.798-7.995C2.566.944 1.561 1.266.902 1.565.139 1.908 0 3.08 0 3.768c0 .69.378 5.65.624 6.479.815 2.736 3.713 3.66 6.383 3.364.136-.02.275-.038.415-.056-.138.022-.276.04-.415.056-3.912.58-7.387 2.005-2.83 7.078 5.013 5.19 6.87-1.113 7.823-4.308.953 3.195 2.05 9.271 7.733 4.308 4.267-4.308 1.172-6.498-2.74-7.078a8.741 8.741 0 01-.415-.056c.14.018.279.036.415.056 2.67.297 5.568-.628 6.383-3.364.246-.828.624-5.79.624-6.478 0-.69-.139-1.861-.902-2.206-.659-.298-1.664-.62-4.3 1.24C16.046 4.748 13.087 8.687 12 10.8z" fill="#1185fe"/> </svg> <span>Sign in</span> </button> <!-- Create Account Button - Blue --> <button type="button" data-action="click->bluesky-auth#handleCreateAccount" data-bluesky-auth-target="signupButton" class="bsky-signup-btn w-full flex items-center justify-center gap-3 px-6 py-3 bg-blue-600 hover:bg-blue-700 font-semibold rounded transition-colors border border-blue-600" style="color: #ffffff !important;"> <svg viewBox="0 0 24 24" width="20" height="20" fill="currentColor" style="color: #ffffff !important;"> <path d="M12 10.8c-1.087-2.114-4.046-6.053-6.798-7.995C2.566.944 1.561 1.266.902 1.565.139 1.908 0 3.08 0 3.768c0 .69.378 5.65.624 6.479.815 2.736 3.713 3.66 6.383 3.364.136-.02.275-.038.415-.056-.138.022-.276.04-.415.056-3.912.58-7.387 2.005-2.83 7.078 5.013 5.19 6.87-1.113 7.823-4.308.953 3.195 2.05 9.271 7.733 4.308 4.267-4.308 1.172-6.498-2.74-7.078a8.741 8.741 0 01-.415-.056c.14.018.279.036.415.056 2.67.297 5.568-.628 6.383-3.364.246-.828.624-5.79.624-6.478 0-.69-.139-1.861-.902-2.206-.659-.298-1.664-.62-4.3 1.24C16.046 4.748 13.087 8.687 12 10.8z" fill="currentColor"/> </svg> <span style="color: #ffffff !important;">Create an account</span> </button> </div> </div> </div> </div> <div data-controller="cookie-consent" data-turbo-permanent id="cookie-consent-banner"> <div data-cookie-consent-target="banner" class="hidden fixed bottom-0 inset-x-0 z-[9999] p-4 sm:p-6"> <div class="mx-auto max-w-3xl rounded-xl bg-white shadow-2xl ring-1 ring-gray-200 p-4 sm:p-6 flex flex-col sm:flex-row items-start sm:items-center gap-3 sm:gap-4"> <div class="flex-1 text-sm text-gray-700 leading-relaxed"> We use cookies to analyze site traffic and improve your experience. You can accept or reject non-essential cookies. </div> <div class="flex gap-2 shrink-0 w-full sm:w-auto"> <button data-action="cookie-consent#reject" class="flex-1 sm:flex-none px-4 py-2 text-sm font-medium text-gray-700 bg-gray-100 hover:bg-gray-200 rounded-lg transition"> Reject </button> <button data-action="cookie-consent#accept" class="flex-1 sm:flex-none px-4 py-2 text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 rounded-lg transition"> Accept </button> </div> </div> </div> </div> </body> </html>