···11+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" viewBox="0 0 1000 600">
22+<rect width="1000" height="600" fill="#002b7f"/>
33+<rect y="300" width="1000" height="300" fill="#ce1126"/>
44+<g fill="#ffd83d" stroke="#000" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
55+<g id="half">
66+<path d="M 216.37706,122.2886 L 214.53882,184.7886 L 151.1197,184.7886 C 143.31716,169.62032 136.87338,156.73735 136.87338,139.29228 C 136.87338,124.69269 148.35647,113.09742 164.90647,113.09742 C 182.42619,113.09742 201.67512,119.001 216.37706,122.2886 z" stroke="none"/>
77+<g stroke-width="1.5">
88+<path d="M 144.45611,125.16084 V 161.23621"/>
99+<path d="M 149.54956,121.47933 V 169.51333"/>
1010+<path d="M 154.64301,118.95176 V 176.17693"/>
1111+<path d="M 159.73645,117.77081 V 162.65201"/>
1212+<path d="M 164.82989,117.54815 V 162.87466"/>
1313+<path d="M 169.92335,115.96463 V 162.84973"/>
1414+<path d="M 175.01679,115.96463 V 162.84973"/>
1515+<path d="M 180.11024,116.42419 V 163.30929"/>
1616+<path d="M 185.20367,116.88375 V 163.76885"/>
1717+<path d="M 190.29712,119.87088 V 166.75598"/>
1818+<path d="M 195.39057,120.33044 V 173.1898"/>
1919+<path d="M 200.48402,120.33044 V 167.21554"/>
2020+<path d="M 205.57747,120.33044 V 167.21554"/>
2121+<path d="M 176.39544,117.92279 C 187.15891,119.69824 210.80151,130.75993 208.105,145.72611 C 204.28366,166.93537 191.89725,158.42373 175.47632,155.37684 L 163.06823,159.51287 C 158.62839,164.04466 152.09048,168.19641 147.67301,162.72978 L 140.32007,162.72978 L 140.32007,191.4522 L 221.66198,191.4522 L 221.66198,122.05882 z" fill="#000"/>
2222+</g>
2323+<circle cx="212.81549" cy="112.98255" r="4.94026"/>
2424+<circle cx="201.713" cy="110.31052" r="4.94026"/>
2525+<circle cx="190.44973" cy="107.48178" r="4.94026"/>
2626+<circle cx="179.14295" cy="105.59596" r="4.94026"/>
2727+<circle cx="167.83617" cy="104.48071" r="4.94026"/>
2828+<circle cx="156.74931" cy="105.11261" r="4.94026"/>
2929+<circle cx="146.17946" cy="108.73163" r="4.94026"/>
3030+<circle cx="137.27551" cy="115.28035" r="4.94026"/>
3131+<circle cx="130.95657" cy="124.41408" r="4.94026"/>
3232+<circle cx="127.912" cy="135.15627" r="4.94026"/>
3333+<circle cx="128.02689" cy="146.30057" r="4.94026"/>
3434+<circle cx="130.15236" cy="157.21509" r="4.94026"/>
3535+<path d="M 214.99838,119.53125 L 214.53882,125.96507 C 202.24782,124.08194 184.82391,117.23346 168.58294,117.23346 C 153.57656,117.23346 141.92853,123.23616 141.92853,138.37316 C 141.92853,153.29163 148.24631,166.85838 156.63441,180.65257 L 147.90279,184.7886 C 140.10025,169.62032 133.65647,156.73735 133.65647,139.29228 C 133.65647,124.69269 145.13956,110.34007 164.90647,110.34007 C 182.42619,110.34007 200.29644,116.24365 214.99838,119.53125 z"/>
3636+</g>
3737+<use xlink:href="#half" transform="translate(443.9375) scale(-1,1)"/>
3838+<path d="M 221.96875,53.125 L 216.8125,62.78125 L 221.96875,72.40625 L 227.125,62.78125 L 221.96875,53.125 z M 221.96875,77.5 L 216.8125,87.125 L 221.96875,96.78125 L 227.125,87.125 L 221.96875,77.5 z"/>
3939+<path d="M 203.58731,74.953125 L 211.71883,80.109375 L 219.82403,74.953125 L 211.71883,69.796875 L 203.58731,74.953125 z M 224.11347,74.953125 L 232.21867,80.109375 L 240.35019,74.953125 L 232.21867,69.796875 L 224.11347,74.953125 z"/>
4040+<circle cx="221.96875" cy="75.06893" r="3.90625"/>
4141+<circle cx="221.96875" cy="100" r="10.45496"/>
4242+<path d="M 219.34375,89.875 C 219.34375,92.989438 219.32163,94.799139 219.3125,96.5 C 216.79207,96.611865 214.30676,96.876712 211.875,97.28125 M 232.0625,97.28125 C 229.63074,96.876712 227.14543,96.611865 224.625,96.5 C 224.625,94.801655 224.625,92.997132 224.625,89.875 M 211.84375,102.5625 C 215.13846,102.01215 218.51788,101.71875 221.96875,101.71875 C 225.41962,101.71875 228.79904,102.01215 232.09375,102.5625" fill="none" stroke-width="1.5"/>
4343+<path d="M 211.75,117.6875 C 210.75812,134.76968 208.74143,152.16667 202.09375,164.8125 L 212.90625,160.4375 C 216.68272,146.10873 217.47609,127.59492 218.625,118.84375 L 211.75,117.6875 z M 232.1875,117.6875 L 225.3125,118.84375 C 226.4614,127.59492 227.25478,146.10873 231.03125,160.4375 L 241.84375,164.8125 C 235.19607,152.16667 233.17938,134.76968 232.1875,117.6875 z"/>
4444+<path d="M 221.95313,154.6875 C 209.03974,154.6875 199.55398,160.77412 198.98438,176.28125 C 195.82773,170.72636 182.4742,153.25552 170.01563,155.59375 C 162.60576,156.98444 156.05854,167.25962 157.17188,179.03125 C 151.037,161.40135 133.06509,158.51225 119.95313,168.9375 C 131.59529,178.51001 136.77421,206.77225 146.57813,219.03125 L 221.95313,219.03125 L 297.32813,219.03125 C 307.13204,206.77225 312.34222,178.51001 323.98438,168.9375 C 310.87241,158.51225 292.90051,161.40135 286.76563,179.03125 C 287.87896,167.25962 281.3005,156.98444 273.89063,155.59375 C 261.43205,153.25552 248.10978,170.72636 244.95313,176.28125 C 244.38354,160.77412 234.86652,154.6875 221.95313,154.6875 z"/>
4545+<g stroke-width="1.5">
4646+<path d="M 297.10662,219.02573 C 297.10662,224.60663 263.44486,230.74447 221.96875,230.74447 C 180.49265,230.74447 146.83089,224.60663 146.83089,219.02573 C 146.83089,213.44487 180.49265,210.5239 221.96875,210.5239 C 263.44486,210.5239 297.10662,213.44487 297.10662,219.02573 z" fill="#000"/>
4747+<circle cx="221.96875" cy="114.44459" r="3.50414"/>
4848+<circle cx="221.96875" cy="122.02731" r="3.73392"/>
4949+<circle cx="221.96875" cy="130.18447" r="4.07859"/>
5050+<circle cx="221.96875" cy="139.26075" r="4.65303"/>
5151+<circle cx="221.96875" cy="149.37105" r="5.11259"/>
5252+<path d="M 219.93751,159.20588 C 219.38456,159.19901 218.86165,159.6649 219.00001,160.54963 C 219.16297,161.59315 219.36706,163.54482 219.56251,164.86213 C 219.78339,166.35532 220.65373,165.99233 220.87501,164.83088 C 221.09631,163.66986 221.00709,162.925 221.06251,160.76838 C 221.09024,159.69029 220.49047,159.21276 219.93751,159.20588 z M 224.00001,159.20588 C 223.44705,159.21276 222.8473,159.69029 222.87501,160.76838 C 222.93043,162.925 222.84123,163.66986 223.06251,164.83088 C 223.28379,165.99233 224.15415,166.35532 224.37501,164.86213 C 224.57046,163.54482 224.77457,161.59315 224.93751,160.54963 C 225.07587,159.6649 224.55298,159.19901 224.00001,159.20588 z M 216.31251,159.76838 C 215.80668,159.83874 215.2822,160.34892 215.31251,160.89338 C 215.36753,161.88856 215.64291,163.0823 215.75001,164.58088 C 215.86046,166.12907 216.95166,165.52771 217.06251,164.64338 C 217.17296,163.75866 217.29793,162.45118 217.18751,160.73713 C 217.1461,160.09422 216.8653,159.8115 216.53126,159.76838 C 216.46168,159.75941 216.38477,159.75833 216.31251,159.76838 z M 227.40626,159.76838 C 227.07222,159.8115 226.79144,160.09422 226.75001,160.73713 C 226.63959,162.45118 226.76458,163.75866 226.87501,164.64338 C 226.98586,165.52771 228.07708,166.12907 228.18751,164.58088 C 228.29461,163.0823 228.57001,161.88856 228.62501,160.89338 C 228.65532,160.34892 228.13086,159.83874 227.62501,159.76838 C 227.55275,159.75833 227.47586,159.75941 227.40626,159.76838 z M 211.96876,161.51838 C 211.50567,161.63805 211.0804,162.19494 211.21876,162.83088 C 211.49507,164.10277 211.75001,165.50899 211.75001,166.79963 C 211.75002,167.90566 212.69459,167.511 212.75001,166.73713 C 212.80503,165.96284 212.90626,164.32364 212.90626,162.83088 C 212.90627,161.71632 212.43186,161.39872 211.96876,161.51838 z M 231.65626,161.51838 C 231.32747,161.60947 231.03126,161.99496 231.03126,162.83088 C 231.03126,164.32364 231.13251,165.96284 231.18751,166.73713 C 231.24293,167.511 232.18752,167.90566 232.18751,166.79963 C 232.18751,165.50899 232.44247,164.10277 232.71876,162.83088 C 232.85712,162.19494 232.43187,161.63805 231.96876,161.51838 C 231.85299,161.48846 231.76586,161.48802 231.65626,161.51838 z M 172.56251,161.68934 C 172.00845,161.67094 171.61973,162.42254 172.00001,163.25184 C 172.60804,164.57916 173.25407,165.41659 173.87501,166.84559 C 174.42803,168.11748 175.2763,167.32448 175.00001,166.43934 C 174.7233,165.55461 174.42308,164.56923 173.59376,162.68934 C 173.28263,161.98423 172.89495,161.70039 172.56251,161.68934 z M 271.37501,161.68934 C 271.04257,161.70039 270.65489,161.98423 270.34376,162.68934 C 269.51444,164.56923 269.21422,165.55461 268.93751,166.43934 C 268.66122,167.32448 269.50949,168.11748 270.06251,166.84559 C 270.68345,165.41659 271.32948,164.57916 271.93751,163.25184 C 272.31779,162.42254 271.92907,161.67094 271.37501,161.68934 z M 176.56251,161.75184 C 176.15469,161.8763 175.90747,162.39361 176.15626,163.00184 C 176.65427,164.21831 177.37284,165.46536 177.87501,166.78309 C 178.31718,167.94413 179.3009,167.61494 178.96876,166.56434 C 178.63704,165.51375 178.53846,164.89884 177.87501,162.90809 C 177.54309,161.91293 176.97034,161.62739 176.56251,161.75184 z M 267.06251,161.75184 C 266.71381,161.82629 266.31145,162.16172 266.06251,162.90809 C 265.39906,164.89884 265.30048,165.51375 264.96876,166.56434 C 264.63662,167.61494 265.62034,167.94413 266.06251,166.78309 C 266.56468,165.46536 267.28325,164.21831 267.78126,163.00184 C 268.03005,162.39361 267.78283,161.8763 267.37501,161.75184 C 267.27305,161.72073 267.17874,161.72702 267.06251,161.75184 z M 181.56251,162.72059 C 181.04838,162.81843 180.65026,163.60277 181.00001,164.56434 C 181.4426,165.78081 182.072,166.93202 182.62501,168.31434 C 183.17803,169.69667 184.0963,169.41781 183.87501,168.53309 C 183.65374,167.64836 183.38263,166.18221 182.71876,163.97059 C 182.42851,163.00283 181.9624,162.6445 181.56251,162.72059 z M 262.37501,162.72059 C 261.97512,162.6445 261.50901,163.00283 261.21876,163.97059 C 260.55489,166.18221 260.28378,167.64836 260.06251,168.53309 C 259.84122,169.41781 260.75949,169.69667 261.31251,168.31434 C 261.86552,166.93202 262.49492,165.78081 262.93751,164.56434 C 263.28726,163.60277 262.88914,162.81843 262.37501,162.72059 z M 169.18751,163.40809 C 168.63064,163.46496 168.12339,164.37333 168.46876,165.06434 C 169.0222,166.17037 169.37242,166.54665 169.90626,167.72059 C 170.45928,168.93706 171.06504,167.99489 170.84376,167.22059 C 170.62248,166.4463 170.33882,165.54493 170.06251,164.43934 C 169.85999,163.62937 169.52164,163.37397 169.18751,163.40809 z M 274.75001,163.40809 C 274.41588,163.37397 274.07753,163.62937 273.87501,164.43934 C 273.5987,165.54493 273.31504,166.4463 273.09376,167.22059 C 272.87248,167.99489 273.47824,168.93706 274.03126,167.72059 C 274.5651,166.54665 274.91532,166.17037 275.46876,165.06434 C 275.81413,164.37333 275.30688,163.46496 274.75001,163.40809 z M 207.71876,166.20588 C 207.13501,166.20536 206.51143,166.98024 206.71876,167.70588 C 207.05049,168.86734 207.56328,169.89687 207.93751,171.39338 C 208.26924,172.72029 209.21168,172.05937 209.15626,171.17463 C 209.10085,170.28991 208.94926,169.0862 208.56251,167.20588 C 208.41734,166.50092 208.06902,166.2062 207.71876,166.20588 z M 236.21876,166.20588 C 235.8685,166.2062 235.5202,166.50092 235.37501,167.20588 C 234.98826,169.0862 234.83669,170.28991 234.78126,171.17463 C 234.72584,172.05937 235.6683,172.72029 236.00001,171.39338 C 236.37424,169.89687 236.88705,168.86734 237.21876,167.70588 C 237.42609,166.98024 236.80253,166.20536 236.21876,166.20588 z M 221.96876,167.39338 C 221.24958,167.39339 221.15023,168.35899 221.12501,169.14338 C 221.04236,171.71466 219.97517,174.21192 219.06251,175.45588 C 218.15028,176.70025 216.80689,176.36933 215.56251,175.70588 C 214.31815,175.04245 213.5769,174.54522 212.25001,173.54963 C 210.9227,172.55448 209.91641,173.13633 211.50001,175.39338 C 216.11747,181.97369 221.12501,187.5982 221.12501,198.33088 C 221.12502,199.71862 221.36747,200.14338 221.96876,200.14338 C 222.57005,200.14338 222.84374,199.71862 222.84376,198.33088 C 222.84376,187.5982 227.82007,181.97369 232.43751,175.39338 C 234.02111,173.13633 233.01484,172.55448 231.68751,173.54963 C 230.36062,174.54522 229.61939,175.04245 228.37501,175.70588 C 227.13063,176.36933 225.78726,176.70025 224.87501,175.45588 C 223.96235,174.21192 222.89518,171.71466 222.81251,169.14338 C 222.78729,168.35899 222.68796,167.39339 221.96876,167.39338 z M 186.68751,165.78309 C 186.62459,165.78137 186.55597,165.79672 186.50001,165.81434 C 186.23143,165.89895 186.05249,166.22315 186.15626,166.78309 C 186.34172,167.78535 187.03621,169.7496 187.31251,170.68934 C 187.58924,171.62952 188.74506,171.42956 188.46876,170.15809 C 188.19206,168.88621 188.20674,168.50208 187.87501,167.06434 C 187.69361,166.27808 187.12805,165.79517 186.68751,165.78309 z M 257.25001,165.78309 C 256.80947,165.79517 256.24393,166.27808 256.06251,167.06434 C 255.73078,168.50208 255.74548,168.88621 255.46876,170.15809 C 255.19246,171.42956 256.3483,171.62952 256.62501,170.68934 C 256.90131,169.7496 257.5958,167.78535 257.78126,166.78309 C 257.88503,166.22315 257.70609,165.89895 257.43751,165.81434 C 257.38155,165.79672 257.31295,165.78137 257.25001,165.78309 z M 165.71876,166.18934 C 165.13997,166.23465 164.72491,166.96181 165.31251,168.03309 C 165.94637,169.18996 167.03368,170.64128 167.53126,171.47059 C 168.02886,172.29991 168.88969,171.74036 168.28126,170.68934 C 167.67324,169.63877 167.46889,168.4321 166.75001,166.93934 C 166.48045,166.37941 166.06604,166.16216 165.71876,166.18934 z M 278.21876,166.18934 C 277.87148,166.16216 277.45707,166.37941 277.18751,166.93934 C 276.46863,168.4321 276.26428,169.63877 275.65626,170.68934 C 275.04783,171.74036 275.90866,172.29991 276.40626,171.47059 C 276.90384,170.64128 277.99115,169.18996 278.62501,168.03309 C 279.21261,166.96181 278.79755,166.23465 278.21876,166.18934 z M 134.03126,169.51838 C 133.36345,169.4829 133.15409,170.68863 133.78126,171.48713 C 134.69393,172.64817 135.32901,173.26405 136.37501,174.20588 C 137.20474,174.95227 137.44592,173.98351 137.03126,173.23713 C 136.61662,172.49076 136.01682,171.7315 135.18751,170.48713 C 134.73288,169.80512 134.33481,169.53451 134.03126,169.51838 z M 309.90626,169.51838 C 309.60271,169.53451 309.20464,169.80512 308.75001,170.48713 C 307.9207,171.7315 307.3209,172.49076 306.90626,173.23713 C 306.4916,173.98351 306.73278,174.95227 307.56251,174.20588 C 308.60851,173.26405 309.24359,172.64817 310.15626,171.48713 C 310.78343,170.68863 310.57407,169.4829 309.90626,169.51838 z M 139.06251,170.51838 C 138.53029,170.52532 138.17911,170.91843 138.59376,171.83088 C 139.18763,173.13695 139.97076,174.61654 140.46876,175.61213 C 140.96636,176.60732 142.48303,176.94156 141.87501,175.39338 C 141.26659,173.84521 141.14677,173.04988 140.59376,171.61213 C 140.31726,170.89327 139.59474,170.51145 139.06251,170.51838 z M 304.87501,170.51838 C 304.34278,170.51145 303.62026,170.89327 303.34376,171.61213 C 302.79075,173.04988 302.67093,173.84521 302.06251,175.39338 C 301.45449,176.94156 302.97116,176.60732 303.46876,175.61213 C 303.96676,174.61654 304.74989,173.13695 305.34376,171.83088 C 305.75841,170.91843 305.40723,170.52532 304.87501,170.51838 z M 129.31251,170.86213 C 128.53874,170.87003 128.47208,171.6458 129.15626,172.14338 C 130.06892,172.80684 131.15649,173.54322 132.21876,174.39338 C 133.46273,175.38856 133.9447,174.4731 133.28126,173.64338 C 132.61782,172.81408 131.97153,171.9731 130.31251,171.14338 C 129.89776,170.93606 129.57043,170.8595 129.31251,170.86213 z M 314.62501,170.86213 C 314.36709,170.8595 314.03976,170.93606 313.62501,171.14338 C 311.96599,171.9731 311.3197,172.81408 310.65626,173.64338 C 309.99282,174.4731 310.47479,175.38856 311.71876,174.39338 C 312.78103,173.54322 313.86859,172.80684 314.78126,172.14338 C 315.46544,171.6458 315.39877,170.87003 314.62501,170.86213 z M 204.96876,171.58088 C 204.5358,171.64145 204.16235,172.13322 204.34376,172.76838 C 204.67549,173.92942 205.05205,175.15983 205.28126,176.36213 C 205.50256,177.52359 206.8463,177.53773 206.62501,176.48713 C 206.40415,175.43655 206.29299,174.12199 205.90626,172.51838 C 205.76109,171.91704 205.45795,171.62817 205.15626,171.58088 C 205.09342,171.57104 205.03061,171.57223 204.96876,171.58088 z M 238.78126,171.58088 C 238.47957,171.62817 238.17645,171.91704 238.03126,172.51838 C 237.64453,174.12199 237.53339,175.43655 237.31251,176.48713 C 237.09122,177.53773 238.43498,177.52359 238.65626,176.36213 C 238.88547,175.15983 239.26205,173.92942 239.59376,172.76838 C 239.77517,172.13322 239.40174,171.64145 238.96876,171.58088 C 238.90691,171.57223 238.84412,171.57104 238.78126,171.58088 z M 174.87501,169.59559 C 174.53504,169.68956 174.33942,170.33955 174.59376,171.25184 C 176.004,176.31148 176.4363,178.97621 176.18751,180.22059 C 175.93873,181.46457 175.256,181.63524 174.34376,181.22059 C 173.43153,180.80594 172.36056,180.20736 171.53126,179.62684 C 170.70196,179.04592 169.83939,179.74084 171.03126,180.72059 C 177.08606,185.69729 181.28306,191.38482 182.90626,198.28309 C 183.238,199.69294 184.12381,199.85878 183.87501,198.28309 C 182.5027,189.59329 182.23882,182.44969 184.31251,177.97059 C 185.09182,176.28738 184.30689,174.67095 183.06251,177.40809 C 182.23321,179.23301 180.89102,179.95295 179.81251,178.37684 C 178.7344,176.80116 176.5072,172.59717 175.84376,170.68934 C 175.59482,169.97391 175.28778,169.64292 175.03126,169.59559 C 174.97783,169.58574 174.92358,169.58217 174.87501,169.59559 z M 268.90626,169.59559 C 268.64974,169.64292 268.3427,169.97391 268.09376,170.68934 C 267.43032,172.59717 265.20312,176.80116 264.12501,178.37684 C 263.0465,179.95295 261.70431,179.23301 260.87501,177.40809 C 259.63063,174.67095 258.8457,176.28738 259.62501,177.97059 C 261.6987,182.44969 261.43482,189.59329 260.06251,198.28309 C 259.81371,199.85878 260.69952,199.69294 261.03126,198.28309 C 262.65446,191.38482 266.85146,185.69729 272.90626,180.72059 C 274.09813,179.74084 273.23556,179.04592 272.40626,179.62684 C 271.57696,180.20736 270.50599,180.80594 269.59376,181.22059 C 268.68152,181.63524 267.99879,181.46457 267.75001,180.22059 C 267.50122,178.97621 267.93352,176.31148 269.34376,171.25184 C 269.5981,170.33955 269.40248,169.68956 269.06251,169.59559 C 269.01394,169.58217 268.95969,169.58574 268.90626,169.59559 z M 144.43751,172.01838 C 143.89144,172.02532 143.43872,172.40782 143.68751,173.23713 C 144.08634,174.5657 144.91121,176.00157 145.18751,176.83088 C 145.46423,177.66021 146.35337,177.27317 146.18751,176.11213 C 146.02167,174.95111 145.94005,174.25902 145.71876,172.98713 C 145.60813,172.35141 144.98359,172.01145 144.43751,172.01838 z M 299.50001,172.01838 C 298.95393,172.01145 298.32939,172.35141 298.21876,172.98713 C 297.99747,174.25902 297.91585,174.95111 297.75001,176.11213 C 297.58415,177.27317 298.47329,177.66021 298.75001,176.83088 C 299.02631,176.00157 299.85118,174.5657 300.25001,173.23713 C 300.4988,172.40782 300.04608,172.02532 299.50001,172.01838 z M 163.21876,170.28309 C 162.58372,170.16913 162.49242,171.80734 163.03126,172.47059 C 163.75015,173.35576 164.74859,174.42538 165.75001,175.62684 C 166.57933,176.62244 167.09175,175.73741 166.59376,174.90809 C 166.09619,174.07837 165.35814,173.19678 164.03126,171.09559 C 163.69944,170.5703 163.43044,170.32108 163.21876,170.28309 z M 280.71876,170.28309 C 280.50708,170.32108 280.23808,170.5703 279.90626,171.09559 C 278.57938,173.19678 277.84133,174.07837 277.34376,174.90809 C 276.84577,175.73741 277.35819,176.62244 278.18751,175.62684 C 279.18893,174.42538 280.18737,173.35576 280.90626,172.47059 C 281.4451,171.80734 281.3538,170.16913 280.71876,170.28309 z M 148.71876,174.23713 C 148.32474,174.20948 147.9904,174.52908 148.15626,175.33088 C 148.38756,176.44733 148.7233,177.37403 149.00001,178.42463 C 149.27633,179.47524 150.00001,179.29522 150.00001,178.29963 C 150.00002,177.30446 149.82212,176.19398 149.65626,175.14338 C 149.57334,174.61809 149.11279,174.26479 148.71876,174.23713 z M 295.21876,174.23713 C 294.82473,174.26479 294.36418,174.61809 294.28126,175.14338 C 294.1154,176.19398 293.9375,177.30446 293.93751,178.29963 C 293.93751,179.29522 294.66119,179.47524 294.93751,178.42463 C 295.21422,177.37403 295.54996,176.44733 295.78126,175.33088 C 295.94712,174.52908 295.61278,174.20948 295.21876,174.23713 z M 161.81251,175.65809 C 161.19365,175.75159 160.6491,176.34985 161.37501,177.28309 C 162.53607,178.77584 163.80743,179.82383 164.25001,180.37684 C 164.69218,180.92987 165.81263,180.72579 165.09376,179.56434 C 164.35906,178.37789 163.2476,176.94908 162.75001,176.06434 C 162.56343,175.73257 162.18383,175.602 161.81251,175.65809 z M 281.87501,175.65809 C 281.60285,175.6867 281.32745,175.81551 281.18751,176.06434 C 280.68992,176.94908 279.57846,178.37789 278.84376,179.56434 C 278.12489,180.72579 279.24534,180.92987 279.68751,180.37684 C 280.13009,179.82383 281.40145,178.77584 282.56251,177.28309 C 283.28842,176.34985 282.74387,175.75159 282.12501,175.65809 C 282.03218,175.64407 281.96573,175.64855 281.87501,175.65809 z M 141.15626,179.83088 C 140.668,179.78654 140.49938,180.18047 141.15626,181.08088 C 142.64903,183.12666 144.29582,186.22273 144.40626,187.54963 C 144.51713,188.87695 143.86095,188.86213 143.25001,188.86213 C 141.42512,188.86215 140.53363,187.53012 138.87501,187.14338 C 137.21599,186.75625 136.97522,187.62865 138.15626,188.48713 C 144.23856,192.91082 150.33501,197.84636 152.78126,202.33088 C 153.77645,204.15578 155.12181,204.82284 154.37501,203.08088 C 152.08795,197.74371 151.53367,193.22426 152.03126,190.48713 C 152.52926,187.75001 153.20794,185.90698 153.12501,184.33088 C 153.0421,182.7552 152.1484,182.86271 151.78126,184.33088 C 151.53247,185.32648 151.05048,186.64538 150.71876,187.14338 C 150.38705,187.64099 149.05314,187.91993 147.78126,186.20588 C 146.50937,184.49186 143.30061,181.19144 142.25001,180.36213 C 141.85605,180.05115 141.44922,179.85749 141.15626,179.83088 z M 302.78126,179.83088 C 302.4883,179.85749 302.08147,180.05115 301.68751,180.36213 C 300.63691,181.19144 297.42815,184.49186 296.15626,186.20588 C 294.88438,187.91993 293.55047,187.64099 293.21876,187.14338 C 292.88704,186.64538 292.40505,185.32648 292.15626,184.33088 C 291.78912,182.86271 290.89542,182.7552 290.81251,184.33088 C 290.72958,185.90698 291.40826,187.75001 291.90626,190.48713 C 292.40385,193.22426 291.84957,197.74371 289.56251,203.08088 C 288.81571,204.82284 290.16107,204.15578 291.15626,202.33088 C 293.60251,197.84636 299.69896,192.91082 305.78126,188.48713 C 306.9623,187.62865 306.72153,186.75625 305.06251,187.14338 C 303.40389,187.53012 302.5124,188.86215 300.68751,188.86213 C 300.07657,188.86213 299.42039,188.87695 299.53126,187.54963 C 299.6417,186.22273 301.28849,183.12666 302.78126,181.08088 C 303.43814,180.18047 303.26952,179.78654 302.78126,179.83088 z" fill="#000" stroke="none"/>
5353+<path d="M 150.12687,212.65106 152.07661,218.82525 M 154.13742,211.48082 155.86506,217.72081 M 158.46968,210.7684 159.65672,217.1334 M 162.48993,209.96408 163.7604,216.31295 M 167.64553,209.45517 168.67848,215.84698 M 173.13007,209.46186 174.24249,215.84031 M 178.48383,207.65149 179.28735,214.07616 M 183.52067,207.97645 184.32419,214.40112 M 188.39503,207.32654 189.19855,213.75121 M 193.28767,206.82164 193.72967,213.28125 M 198.51623,206.81977 198.89973,213.28312 M 203.25656,206.49325 203.58317,212.95973 M 208.48353,206.16701 208.75481,212.63605 M 214.49524,206.00453 214.76652,212.47357 M 293.81064,212.65106 291.8609,218.82525 M 289.80009,211.48082 288.07245,217.72081 M 285.46783,210.7684 284.28079,217.1334 M 281.44758,209.96408 280.17711,216.31295 M 276.29198,209.45517 275.25903,215.84698 M 270.80744,209.46186 269.69502,215.84031 M 265.45368,207.65149 264.65016,214.07616 M 260.41684,207.97645 259.61332,214.40112 M 255.54248,207.32654 254.73896,213.75121 M 250.64984,206.82164 250.20784,213.28125 M 245.42128,206.81977 245.03778,213.28312 M 240.68095,206.49325 240.35434,212.95973 M 235.45398,206.16701 235.1827,212.63605 M 229.44227,206.00453 229.17099,212.47357 M 221.96876,205.58364 221.96876,212.7068"/>
5454+</g>
5555+</g>
5656+</svg>
···11+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 900 600">
22+<rect width="900" height="600" fill="#000"/>
33+<rect width="900" height="206.25" fill="#fff"/>
44+<rect width="900" height="187.5" fill="#007168"/>
55+<rect y="393.5" width="900" height="206.5" fill="#fff"/>
66+<rect y="412.5" width="900" height="187.5" fill="#fce100"/>
77+<path d="M 0,0 V 600 L 393.75,300 z" fill="#d21034"/>
88+<path d="M 189.54603,214.56867 L 128.78569,171.99623 L 69.930531,217.0706 L 91.643351,146.12847 L 30.587837,104.08263 L 104.76744,102.81043 L 125.88822,31.750306 L 150.02091,101.90617 L 224.12978,100.03443 L 164.86501,144.66535 z" transform="matrix(1.221793,2.542786e-2,-2.542786e-2,1.221793,-21.7604,145.5065)" fill="#fce100"/>
99+<path d="M 77.1122,167.913 L 118.946,167.913 C 122.312,171.689 129.822,173.229 136.975,167.868 C 150.087,160.688 175.454,167.913 175.454,167.913 L 180.449,162.606 L 168.273,122.645 L 163.902,117.962 C 163.902,117.962 154.537,112.343 136.741,114.216 C 118.946,116.089 112.702,113.592 112.702,113.592 C 112.702,113.592 97.0927,115.465 92.7219,117.65 C 92.2414,118.039 87.7268,122.645 87.7268,122.645 z" transform="matrix(1.259719,0,0,1.259719,-29.41569,140.9042)" fill="#fff" fill-rule="evenodd" stroke="#000" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
1010+<path d="M 85.5415,157.611 C 85.5415,157.611 125.502,152.616 137.054,167.913 C 130.53,172.377 124.786,172.745 118.634,168.226 C 119.614,166.593 132.995,152.616 167.024,157.299" transform="matrix(1.259719,0,0,1.259719,-29.41569,140.9042)" fill="none" stroke="#000" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
1111+<path d="M 128.312,117.65 L 128,161.67 M 163.902,118.274 L 171.395,153.24 M 93.1339,117.725 L 88.9756,135.757" transform="matrix(1.259719,0,0,1.259719,-29.41569,140.9042)" stroke="#000" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
1212+<path d="M 49.9417,172.706 L 59.7799,184.241 C 60.9352,184.93 61.9614,184.886 62.9876,184.241 L 77.5681,166.745 L 83.6919,159.163 C 84.6401,158.04 84.9428,156.787 84.8583,155.664 L 96.5657,145.261 C 97.5234,145.327 98.0938,145.435 99.0515,145.5 C 97.92,145.207 97.0898,144.656 97.9807,143.416 L 100.605,141.375 L 102.646,143.999 C 102.646,143.999 99.7304,147.79 99.4388,147.79 C 99.1472,147.79 96.2311,147.79 96.2311,147.79 L 90.1073,153.331 L 92.7843,155.697 L 96.8143,166.745 L 101.772,163.245 L 98.5639,151.873 L 105.563,144.291 L 102.938,140.208 L 104.688,137.875 C 104.688,137.875 128.91,153.125 138.242,149.043 C 138.494,149.134 138.806,138.167 138.806,138.167 C 138.806,138.167 113.728,135.543 113.144,130.585 C 112.561,125.628 118.685,125.045 118.685,125.045 L 116.06,121.254 L 116.644,119.212 L 121.018,124.753 L 130.933,116.296 L 189.254,182.783 C 192.438,181.492 193.118,180.713 193.337,177.534 C 193.255,177.454 136.182,111.922 136.182,111.922 L 140.556,107.257 C 141.418,106.285 141.721,105.872 141.722,104.34 L 148.429,98.5082 C 150.446,99.2011 151.732,100.41 152.803,102.007 L 171.242,86.3801 C 171.728,86.8661 173.204,87.3521 174.206,86.8055 L 204.691,57.5442 L 171.466,81.0116 L 170.3,80.1368 C 170.3,79.1647 171.505,78.9243 170.3,77.2207 C 169.009,75.6716 167.092,78.6788 166.801,78.6788 C 166.509,78.6788 161.992,77.0824 161.002,75.06 L 160.677,80.4284 L 152.22,88.3019 L 145.805,88.0103 L 136.473,97.0501 L 135.307,100.549 L 136.765,103.466 C 136.765,103.466 131.807,107.84 131.807,107.548 C 131.807,107.257 130.805,106.259 130.762,106.13 L 135.015,102.299 L 135.598,99.6747 L 134.179,97.4589 C 133.749,97.7714 128.308,103.466 128.016,102.882 C 127.53,102.338 112.27,85.0942 112.27,85.0942 L 113.144,81.8865 L 103.23,71.0969 C 99.6155,69.849 93.8982,69.6389 92.7318,77.5123 C 91.8235,79.3616 84.2751,77.8039 84.2751,77.8039 L 80.1926,78.6788 L 57.1554,111.339 L 69.9862,126.794 L 96.2311,93.5508 L 97.0102,84.1332 L 102.517,90.2905 C 104.353,90.5264 106.102,90.5471 107.766,89.7073 L 123.315,107.063 L 120.726,109.589 C 121.601,110.561 122.203,111.182 123.078,112.154 C 123.953,111.571 124.788,110.871 125.663,110.287 C 125.955,110.677 126.441,111.416 126.733,111.805 C 125.43,112.514 124.516,113.458 123.213,114.167 C 121.128,112.808 119.115,111.116 119.268,108.423 L 110.52,115.713 L 110.228,117.171 L 84.2751,138.75 L 81.9422,139.042 L 81.359,145.749 L 98.2723,131.752 L 98.2723,129.71 L 100.022,131.168 L 113.144,120.671 C 113.144,120.671 114.019,121.837 113.728,121.837 C 113.436,121.837 102.063,132.335 102.063,132.335 L 101.772,133.501 L 99.7304,135.251 L 98.5639,134.376 L 82.817,148.373 L 80.4842,148.373 L 71.7359,157.122 C 69.4795,157.318 67.5243,157.557 65.6121,158.871 z" transform="matrix(1.259719,0,0,1.259719,-29.41569,140.9042)" fill="#000" fill-rule="evenodd" stroke="#000" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
1313+</svg>
···55import { getVideoAmbientBackdropInlineStyle } from "../utils/video-ambient-backdrop";
6677export const server = {
88+ /** Loads the next page of video posts for a handle using the stored cursor. */
89 loadMorePosts: defineAction({
910 input: z.object({
1011 handle: z.string(),
···11{
22+ "index.title": "orbyt – comunidades de video",
33+ "index.meta.description": "una nueva app de video para bluesky",
44+ "index.motto": "comunidades de video",
55+ "index.button.ios": "iOS",
66+ "index.button.android_waitlist": "Lista de espera Android",
77+ "index.schema.organization_description": "una nueva app de video para bluesky",
88+99+ "app.title": "Descarga la app – orbyt",
1010+ "app.subtitle.desktop": "Escanea para descargar la app",
1111+ "app.subtitle.mobile": "Descarga la app",
1212+ "app.qr.alt": "Código QR para descargar la app orbyt",
1313+ "app.qr.caption": "o descarga directamente abajo",
1414+ "app.button.ios": "iOS",
1515+ "app.button.android_waitlist": "Lista de espera Android",
1616+1717+ "404.title": "404 – Página no encontrada | Orbyt",
1818+ "404.heading": "Sin señal",
1919+ "404.message": "Esta página no existe o fue movida.",
220 "404.action.back": "Volver",
2121+ "404.action.home": "Inicio",
2222+2323+ "contact.title": "Contáctanos – Orbyt",
2424+ "contact.logo.alt": "Orbyt",
2525+ "contact.heading": "Contáctanos",
2626+ "contact.section.general": "Consultas generales",
2727+ "contact.section.press": "Prensa y medios",
2828+ "contact.section.bugs": "Reporte de errores",
2929+ "contact.section.copyright": "Derechos de autor y contenido",
3030+ "contact.copyright.html": "Orbyt es un cliente de terceros para Bluesky. El contenido está alojado en la red AT Protocol, no por Orbyt. Para retiradas por derechos de autor o problemas legales relacionados con el contenido, comunícate directamente con el <a href=\"https://bsky.social/about/support\" target=\"_blank\" rel=\"noopener\">soporte de Bluesky</a>.",
3131+3232+ "footer.link.bluesky": "bluesky",
3333+ "footer.link.community": "comunidad",
3434+ "footer.link.terms": "términos",
3535+ "footer.link.privacy": "privacidad",
3636+ "footer.link.contact": "contacto",
3737+3838+ "privacy.title": "Política de privacidad – Orbyt",
3939+ "privacy.heading": "Política de privacidad",
4040+ "privacy.last_updated": "Última actualización: 2 de febrero de 2026",
4141+ "privacy.logo.alt": "Orbyt",
4242+4343+ "terms.title": "Términos de uso – Orbyt",
4444+ "terms.heading": "Términos de uso",
4545+ "terms.last_updated": "Última actualización: 2 de febrero de 2026",
4646+ "terms.logo.alt": "Orbyt",
4747+348 "profile.install_cta": "Descarga la app",
44- "post.install_cta": "Descarga la app"
4949+ "profile.empty_feed": "nada aquí, todavía...",
5050+ "profile.load_more": "Cargar más",
5151+ "profile.loading": "Cargando...",
5252+ "profile.breadcrumb.home": "Inicio",
5353+5454+ "post.install_cta": "Descarga la app",
5555+ "post.breadcrumb.home": "Inicio",
5656+ "post.caption.more": "Más",
5757+5858+ "altstore.tip_label": "consíguelo en AltStore",
5959+ "altstore.aria_label": "Agregar fuente de orbyt en AltStore PAL",
6060+ "altstore.dialog.title": "Escanea con tu iPhone",
6161+ "altstore.dialog.qr_alt": "Código QR para AltStore PAL — escanea con tu iPhone",
6262+ "altstore.dialog.hint": "Agrega la fuente de orbyt en AltStore PAL",
6363+ "altstore.dialog.close": "Cerrar",
6464+ "altstore.dialog.need_altstore": "¿Nuevo en AltStore?",
6565+ "altstore.dialog.get_altstore": "Más información"
566}
+66
src/i18n/translations/es-ES.json
···11+{
22+ "index.title": "orbyt – comunidades de vídeo",
33+ "index.meta.description": "una nueva app de vídeo para bluesky",
44+ "index.motto": "comunidades de vídeo",
55+ "index.button.ios": "iOS",
66+ "index.button.android_waitlist": "Lista de espera Android",
77+ "index.schema.organization_description": "una nueva app de vídeo para bluesky",
88+99+ "app.title": "Consigue la app – orbyt",
1010+ "app.subtitle.desktop": "Escanea para conseguir la app",
1111+ "app.subtitle.mobile": "Consigue la app",
1212+ "app.qr.alt": "Código QR para descargar la app orbyt",
1313+ "app.qr.caption": "o descarga directamente abajo",
1414+ "app.button.ios": "iOS",
1515+ "app.button.android_waitlist": "Lista de espera Android",
1616+1717+ "404.title": "404 – Página no encontrada | Orbyt",
1818+ "404.heading": "Sin señal",
1919+ "404.message": "Esta página no existe o ha sido movida.",
2020+ "404.action.back": "Volver",
2121+ "404.action.home": "Inicio",
2222+2323+ "contact.title": "Contáctanos – Orbyt",
2424+ "contact.logo.alt": "Orbyt",
2525+ "contact.heading": "Contáctanos",
2626+ "contact.section.general": "Consultas generales",
2727+ "contact.section.press": "Prensa y medios",
2828+ "contact.section.bugs": "Informe de errores",
2929+ "contact.section.copyright": "Derechos de autor y contenido",
3030+ "contact.copyright.html": "Orbyt es un cliente de terceros para Bluesky. El contenido está alojado en la red AT Protocol, no por Orbyt. Para retiradas por derechos de autor o problemas legales relacionados con el contenido, comunícate directamente con el <a href=\"https://bsky.social/about/support\" target=\"_blank\" rel=\"noopener\">soporte de Bluesky</a>.",
3131+3232+ "footer.link.bluesky": "bluesky",
3333+ "footer.link.community": "comunidad",
3434+ "footer.link.terms": "términos",
3535+ "footer.link.privacy": "privacidad",
3636+ "footer.link.contact": "contacto",
3737+3838+ "privacy.title": "Política de privacidad – Orbyt",
3939+ "privacy.heading": "Política de privacidad",
4040+ "privacy.last_updated": "Última actualización: 2 de febrero de 2026",
4141+ "privacy.logo.alt": "Orbyt",
4242+4343+ "terms.title": "Términos de uso – Orbyt",
4444+ "terms.heading": "Términos de uso",
4545+ "terms.last_updated": "Última actualización: 2 de febrero de 2026",
4646+ "terms.logo.alt": "Orbyt",
4747+4848+ "profile.install_cta": "Consigue la app",
4949+ "profile.empty_feed": "nada aquí, todavía...",
5050+ "profile.load_more": "Cargar más",
5151+ "profile.loading": "Cargando...",
5252+ "profile.breadcrumb.home": "Inicio",
5353+5454+ "post.install_cta": "Consigue la app",
5555+ "post.breadcrumb.home": "Inicio",
5656+ "post.caption.more": "Más",
5757+5858+ "altstore.tip_label": "consíguelo en AltStore",
5959+ "altstore.aria_label": "Añadir fuente de orbyt en AltStore PAL",
6060+ "altstore.dialog.title": "Escanea con tu iPhone",
6161+ "altstore.dialog.qr_alt": "Código QR para AltStore PAL — escanea con tu iPhone",
6262+ "altstore.dialog.hint": "Añade la fuente de orbyt en AltStore PAL",
6363+ "altstore.dialog.close": "Cerrar",
6464+ "altstore.dialog.need_altstore": "¿Nuevo en AltStore?",
6565+ "altstore.dialog.get_altstore": "Más información"
6666+}
+1-59
src/i18n/translations/es-MX.json
···11{
22- "index.title": "orbyt – comunidades de video",
33- "index.meta.description": "una nueva app de video para bluesky",
44- "index.motto": "comunidades de video",
55- "index.button.ios": "iOS",
66- "index.button.android_waitlist": "Lista de espera Android",
77- "index.schema.organization_description": "una nueva app de video para bluesky",
88-92 "app.title": "Obtén la app – orbyt",
103 "app.subtitle.desktop": "Escanea para obtener la app",
114 "app.subtitle.mobile": "Obtén la app",
1212- "app.qr.alt": "Código QR para descargar la app orbyt",
1313- "app.qr.caption": "o descarga directamente abajo",
1414- "app.button.ios": "iOS",
1515- "app.button.android_waitlist": "Lista de espera Android",
1616-1717- "404.title": "404 – Página no encontrada | Orbyt",
1818- "404.heading": "Sin señal",
1919- "404.message": "Esta página no existe o fue movida.",
2020- "404.action.back": "Regresar",
2121- "404.action.home": "Inicio",
2222-2323- "contact.title": "Contáctanos – Orbyt",
2424- "contact.logo.alt": "Orbyt",
2525- "contact.heading": "Contáctanos",
2626- "contact.section.general": "Consultas generales",
2727- "contact.section.press": "Prensa y medios",
2828- "contact.section.bugs": "Reporte de errores",
2929- "contact.section.copyright": "Derechos de autor y contenido",
3030- "contact.copyright.html": "Orbyt es un cliente de terceros para Bluesky. El contenido está alojado en la red AT Protocol, no por Orbyt. Para retiradas por derechos de autor o problemas legales relacionados con el contenido, comunícate directamente con el <a href=\"https://bsky.social/about/support\" target=\"_blank\" rel=\"noopener\">soporte de Bluesky</a>.",
3131-3232- "footer.link.bluesky": "bluesky",
3333- "footer.link.community": "comunidad",
3434- "footer.link.terms": "términos",
3535- "footer.link.privacy": "privacidad",
3636- "footer.link.contact": "contacto",
3737-3838- "privacy.title": "Política de privacidad – Orbyt",
3939- "privacy.heading": "Política de privacidad",
4040- "privacy.last_updated": "Última actualización: 2 de febrero de 2026",
4141- "privacy.logo.alt": "Orbyt",
4242-4343- "terms.title": "Términos de uso – Orbyt",
4444- "terms.heading": "Términos de uso",
4545- "terms.last_updated": "Última actualización: 2 de febrero de 2026",
4646- "terms.logo.alt": "Orbyt",
4747-485 "profile.install_cta": "Obtén la app",
4949- "profile.empty_feed": "nada aquí, todavía...",
5050- "profile.load_more": "Cargar más",
5151- "profile.loading": "Cargando...",
5252- "profile.breadcrumb.home": "Inicio",
5353-546 "post.install_cta": "Obtén la app",
5555- "post.breadcrumb.home": "Inicio",
5656- "post.caption.more": "Más",
5757-5858- "altstore.tip_label": "consíguelo en AltStore",
5959- "altstore.aria_label": "Agregar fuente de orbyt en AltStore PAL",
6060- "altstore.dialog.title": "Escanea con tu iPhone",
6161- "altstore.dialog.qr_alt": "Código QR para AltStore PAL — escanea con tu iPhone",
6262- "altstore.dialog.hint": "Agrega la fuente de orbyt en AltStore PAL",
6363- "altstore.dialog.close": "Cerrar",
6464- "altstore.dialog.need_altstore": "¿Nuevo en AltStore?",
6565- "altstore.dialog.get_altstore": "Más información"
77+ "404.action.back": "Regresar"
668}
+66
src/i18n/translations/it.json
···11+{
22+ "index.title": "orbyt – comunità video",
33+ "index.meta.description": "una nuova app video per bluesky",
44+ "index.motto": "comunità video",
55+ "index.button.ios": "iOS",
66+ "index.button.android_waitlist": "Lista d'attesa Android",
77+ "index.schema.organization_description": "una nuova app video per bluesky",
88+99+ "app.title": "Scarica l'app – orbyt",
1010+ "app.subtitle.desktop": "Scansiona per scaricare l'app",
1111+ "app.subtitle.mobile": "Scarica l'app",
1212+ "app.qr.alt": "Codice QR per scaricare l'app orbyt",
1313+ "app.qr.caption": "o scarica direttamente qui sotto",
1414+ "app.button.ios": "iOS",
1515+ "app.button.android_waitlist": "Lista d'attesa Android",
1616+1717+ "404.title": "404 – Pagina non trovata | Orbyt",
1818+ "404.heading": "Nessun segnale",
1919+ "404.message": "Questa pagina non esiste o è stata spostata.",
2020+ "404.action.back": "Indietro",
2121+ "404.action.home": "Home",
2222+2323+ "contact.title": "Contattaci – Orbyt",
2424+ "contact.logo.alt": "Orbyt",
2525+ "contact.heading": "Contattaci",
2626+ "contact.section.general": "Per domande generali",
2727+ "contact.section.press": "Richieste per stampa e media",
2828+ "contact.section.bugs": "Segnalazioni di bug",
2929+ "contact.section.copyright": "Problemi di copyright e contenuti",
3030+ "contact.copyright.html": "Orbyt è un client di terze parti per Bluesky. I contenuti sono ospitati sulla rete AT Protocol, non da Orbyt. Per richieste di rimozione per violazione del copyright o questioni legali relative ai contenuti, contatta direttamente il <a href=\"https://bsky.social/about/support\" target=\"_blank\" rel=\"noopener\">supporto di Bluesky</a>.",
3131+3232+ "footer.link.bluesky": "bluesky",
3333+ "footer.link.community": "comunità",
3434+ "footer.link.terms": "termini",
3535+ "footer.link.privacy": "privacy",
3636+ "footer.link.contact": "contatti",
3737+3838+ "privacy.title": "Informativa sulla privacy – Orbyt",
3939+ "privacy.heading": "Informativa sulla privacy",
4040+ "privacy.last_updated": "Ultimo aggiornamento: 2 febbraio 2026",
4141+ "privacy.logo.alt": "Orbyt",
4242+4343+ "terms.title": "Termini di utilizzo – Orbyt",
4444+ "terms.heading": "Termini di utilizzo",
4545+ "terms.last_updated": "Ultimo aggiornamento: 2 febbraio 2026",
4646+ "terms.logo.alt": "Orbyt",
4747+4848+ "profile.install_cta": "Scarica l'app",
4949+ "profile.empty_feed": "ancora nulla qui...",
5050+ "profile.load_more": "Carica altro",
5151+ "profile.loading": "Caricamento...",
5252+ "profile.breadcrumb.home": "Home",
5353+5454+ "post.install_cta": "Scarica l'app",
5555+ "post.breadcrumb.home": "Home",
5656+ "post.caption.more": "Altro",
5757+5858+ "altstore.tip_label": "scaricalo su altstore",
5959+ "altstore.aria_label": "Aggiungi la fonte orbyt su AltStore PAL",
6060+ "altstore.dialog.title": "Scansiona con il tuo iPhone",
6161+ "altstore.dialog.qr_alt": "Codice QR per AltStore PAL — scansiona con il tuo iPhone",
6262+ "altstore.dialog.hint": "Aggiunge la fonte orbyt su AltStore PAL",
6363+ "altstore.dialog.close": "Chiudi",
6464+ "altstore.dialog.need_altstore": "Nuovo su AltStore?",
6565+ "altstore.dialog.get_altstore": "Scopri di più"
6666+}
+66
src/i18n/translations/tr.json
···11+{
22+ "index.title": "orbyt – video toplulukları",
33+ "index.meta.description": "bluesky için yeni bir video uygulaması",
44+ "index.motto": "video toplulukları",
55+ "index.button.ios": "iOS",
66+ "index.button.android_waitlist": "Android Bekleme Listesi",
77+ "index.schema.organization_description": "bluesky için yeni bir video uygulaması",
88+99+ "app.title": "Uygulamayı İndir – orbyt",
1010+ "app.subtitle.desktop": "Uygulamayı indirmek için tarat",
1111+ "app.subtitle.mobile": "Uygulamayı indir",
1212+ "app.qr.alt": "orbyt uygulamasını indirmek için QR kodu",
1313+ "app.qr.caption": "ya da aşağıdan doğrudan indir",
1414+ "app.button.ios": "iOS",
1515+ "app.button.android_waitlist": "Android Bekleme Listesi",
1616+1717+ "404.title": "404 – Sayfa Bulunamadı | Orbyt",
1818+ "404.heading": "Sinyal Yok",
1919+ "404.message": "Bu sayfa mevcut değil veya taşındı.",
2020+ "404.action.back": "Geri Dön",
2121+ "404.action.home": "Ana Sayfa",
2222+2323+ "contact.title": "Bize Ulaşın – Orbyt",
2424+ "contact.logo.alt": "Orbyt",
2525+ "contact.heading": "Bize Ulaşın",
2626+ "contact.section.general": "Genel sorular için",
2727+ "contact.section.press": "Basın ve medya talepleri",
2828+ "contact.section.bugs": "Hata bildirimleri",
2929+ "contact.section.copyright": "Telif hakkı ve içerik sorunları",
3030+ "contact.copyright.html": "Orbyt, Bluesky için bir üçüncü taraf istemcisidir. İçerikler AT Protocol ağında barındırılmakta olup Orbyt tarafından barındırılmamaktadır. Telif hakkı ihlalleri veya içerikle ilgili hukuki sorunlar için lütfen doğrudan <a href=\"https://bsky.social/about/support\" target=\"_blank\" rel=\"noopener\">Bluesky Destek</a> ile iletişime geçin.",
3131+3232+ "footer.link.bluesky": "bluesky",
3333+ "footer.link.community": "topluluk",
3434+ "footer.link.terms": "koşullar",
3535+ "footer.link.privacy": "gizlilik",
3636+ "footer.link.contact": "iletişim",
3737+3838+ "privacy.title": "Gizlilik Politikası – Orbyt",
3939+ "privacy.heading": "Gizlilik Politikası",
4040+ "privacy.last_updated": "Son güncelleme: 2 Şubat 2026",
4141+ "privacy.logo.alt": "Orbyt",
4242+4343+ "terms.title": "Kullanım Koşulları – Orbyt",
4444+ "terms.heading": "Kullanım Koşulları",
4545+ "terms.last_updated": "Son güncelleme: 2 Şubat 2026",
4646+ "terms.logo.alt": "Orbyt",
4747+4848+ "profile.install_cta": "Uygulamayı İndir",
4949+ "profile.empty_feed": "henüz burada bir şey yok...",
5050+ "profile.load_more": "Daha fazla yükle",
5151+ "profile.loading": "Yükleniyor...",
5252+ "profile.breadcrumb.home": "Ana Sayfa",
5353+5454+ "post.install_cta": "Uygulamayı İndir",
5555+ "post.breadcrumb.home": "Ana Sayfa",
5656+ "post.caption.more": "Daha fazla",
5757+5858+ "altstore.tip_label": "altstore'da edinin",
5959+ "altstore.aria_label": "AltStore PAL'e orbyt kaynağını ekle",
6060+ "altstore.dialog.title": "iPhone'unuzla tarayın",
6161+ "altstore.dialog.qr_alt": "AltStore PAL için QR kodu — iPhone'unuzla tarayın",
6262+ "altstore.dialog.hint": "AltStore PAL'e orbyt kaynağını ekler",
6363+ "altstore.dialog.close": "Kapat",
6464+ "altstore.dialog.need_altstore": "AltStore'da yeni misiniz?",
6565+ "altstore.dialog.get_altstore": "Daha fazla bilgi"
6666+}
+144-7
src/i18n/utils.ts
···11import en from './translations/en.json'
22import de from './translations/de.json'
33import esMX from './translations/es-MX.json'
44+import esES from './translations/es-ES.json'
55+import es419 from './translations/es-419.json'
46import fr from './translations/fr.json'
77+import it from './translations/it.json'
58import ja from './translations/ja.json'
69import ko from './translations/ko.json'
710import ptBR from './translations/pt-BR.json'
88-import es419 from './translations/es-419.json'
1111+import tr from './translations/tr.json'
9121013export const DEFAULT_LOCALE = 'en'
11141212-export const SUPPORTED_LOCALES = ['de', 'es-MX', 'fr', 'ja', 'ko', 'pt-BR', 'es-419'] as const
1515+export const SUPPORTED_LOCALES = ['de', 'es-MX', 'es-ES', 'es-419', 'fr', 'it', 'ja', 'ko', 'pt-BR', 'tr'] as const
1316export type SupportedLocale = typeof SUPPORTED_LOCALES[number] | 'en'
14171518export const LOCALE_LABELS: Record<string, string> = {
1619 en: 'English',
1720 de: 'Deutsch',
1821 'es-MX': 'Español (MX)',
2222+ 'es-ES': 'Español (ES)',
2323+ 'es-419': 'Español (LA)',
1924 fr: 'Français',
2525+ it: 'Italiano',
2026 ja: '日本語',
2127 ko: '한국어',
2228 'pt-BR': 'Português',
2323- 'es-419': 'Español (LA)',
2929+ tr: 'Türkçe',
2430}
25312632export const LOCALE_CODES: Record<string, string> = {
2733 en: 'EN',
2834 de: 'DE',
2935 'es-MX': 'ES',
3636+ 'es-ES': 'ES',
3737+ 'es-419': 'ES',
3038 fr: 'FR',
3939+ it: 'IT',
3140 ja: 'JA',
3241 ko: 'KO',
3342 'pt-BR': 'PT',
3434- 'es-419': 'ES',
4343+ tr: 'TR',
4444+}
4545+4646+/** ISO 3166-1 alpha-2 country code for the default flag SVG per locale */
4747+export const LOCALE_FLAG_COUNTRY: Record<string, string> = {
4848+ en: 'us',
4949+ de: 'de',
5050+ 'es-MX': 'mx',
5151+ 'es-ES': 'es',
5252+ 'es-419': 'ar',
5353+ fr: 'fr',
5454+ it: 'it',
5555+ ja: 'jp',
5656+ ko: 'kr',
5757+ 'pt-BR': 'br',
5858+ tr: 'tr',
5959+}
6060+6161+/**
6262+ * Per-locale map of Cloudflare country code → flag SVG filename stem.
6363+ * Lets every locale show the visitor's own country flag rather than a
6464+ * single representative flag for the entire language region.
6565+ */
6666+export const LOCALE_GEO_FLAGS: Record<string, Record<string, string>> = {
6767+ en: {
6868+ // British Isles
6969+ GB: 'gb', IE: 'ie',
7070+ // North America
7171+ CA: 'ca',
7272+ // Caribbean & Central America
7373+ JM: 'jm', TT: 'tt', BB: 'bb', BS: 'bs', BZ: 'bz', GY: 'gy',
7474+ // Oceania
7575+ AU: 'au', NZ: 'nz', FJ: 'fj', PG: 'pg', SB: 'sb', VU: 'vu',
7676+ WS: 'ws', TO: 'to', KI: 'ki', NR: 'nr',
7777+ // Asia-Pacific
7878+ SG: 'sg', PH: 'ph', MY: 'my', HK: 'hk', IN: 'in',
7979+ PK: 'pk', BD: 'bd', LK: 'lk',
8080+ // Africa
8181+ ZA: 'za', NG: 'ng', GH: 'gh', KE: 'ke', ET: 'et', TZ: 'tz',
8282+ UG: 'ug', ZW: 'zw', ZM: 'zm', RW: 'rw', CM: 'cm', SL: 'sl',
8383+ LR: 'lr', GM: 'gm', BW: 'bw', NA: 'na', LS: 'ls', SZ: 'sz',
8484+ MW: 'mw', SC: 'sc', MU: 'mu',
8585+ // Europe
8686+ MT: 'mt', CY: 'cy',
8787+ },
8888+ de: {
8989+ AT: 'at', CH: 'ch', LI: 'li',
9090+ // German-speaking communities in Belgium and Luxembourg
9191+ BE: 'be', LU: 'lu',
9292+ },
9393+ 'es-MX': {
9494+ // Mexico is the default; include other countries whose speakers
9595+ // might pick es-MX over es-419
9696+ MX: 'mx',
9797+ },
9898+ 'es-419': {
9999+ // Every Spanish-speaking country in Latin America sees their own flag
100100+ MX: 'mx', CO: 'co', AR: 'ar', CL: 'cl', PE: 'pe',
101101+ VE: 've', EC: 'ec', GT: 'gt', CU: 'cu', BO: 'bo',
102102+ DO: 'do', HN: 'hn', PY: 'py', SV: 'sv', NI: 'ni',
103103+ CR: 'cr', PA: 'pa', UY: 'uy', PR: 'pr',
104104+ // Equatorial Guinea (Spanish is official)
105105+ GQ: 'gq',
106106+ },
107107+ fr: {
108108+ // Europe
109109+ BE: 'be', CH: 'ch', LU: 'lu', MC: 'mc',
110110+ // North America
111111+ CA: 'ca',
112112+ // Caribbean
113113+ HT: 'ht',
114114+ // West Africa
115115+ SN: 'sn', CI: 'ci', ML: 'ml', BF: 'bf', NE: 'ne', GN: 'gn',
116116+ TG: 'tg', BJ: 'bj',
117117+ // Central Africa
118118+ CM: 'cm', CD: 'cd', CG: 'cg', CF: 'cf', GA: 'ga', TD: 'td',
119119+ GQ: 'gq', BI: 'bi', RW: 'rw',
120120+ // East Africa & Indian Ocean
121121+ MG: 'mg', MU: 'mu', SC: 'sc', KM: 'km', DJ: 'dj',
122122+ // North Africa
123123+ MA: 'ma', DZ: 'dz', TN: 'tn', MR: 'mr',
124124+ // Pacific
125125+ VU: 'vu',
126126+ },
127127+ it: {
128128+ // Italian-speaking Switzerland (Ticino) and San Marino
129129+ CH: 'ch', SM: 'sm',
130130+ },
131131+ 'pt-BR': {
132132+ PT: 'pt', AO: 'ao', MZ: 'mz', CV: 'cv',
133133+ GW: 'gw', ST: 'st', TL: 'tl', MO: 'mo',
134134+ },
135135+ tr: {
136136+ // Northern Cyprus and Azerbaijan (closely related Turkic language)
137137+ CY: 'cy', AZ: 'az',
138138+ },
139139+}
140140+141141+/** Returns the SVG filename stem (e.g. "gb") for the flag to display. */
142142+export function getFlagCountryCode(locale: string, countryCode?: string): string {
143143+ if (countryCode) {
144144+ const geoFlag = LOCALE_GEO_FLAGS[locale]?.[countryCode]
145145+ if (geoFlag) return geoFlag
146146+ }
147147+ return LOCALE_FLAG_COUNTRY[locale] ?? 'us'
35148}
36149150150+// Emoji flags kept for any non-switcher uses (e.g. schema markup)
37151export const LOCALE_FLAGS: Record<string, string> = {
38152 en: '🇺🇸',
39153 de: '🇩🇪',
40154 'es-MX': '🇲🇽',
155155+ 'es-ES': '🇪🇸',
156156+ 'es-419': '🇦🇷',
41157 fr: '🇫🇷',
158158+ it: '🇮🇹',
42159 ja: '🇯🇵',
43160 ko: '🇰🇷',
44161 'pt-BR': '🇧🇷',
4545- 'es-419': '🇦🇷',
162162+ tr: '🇹🇷',
46163}
47164165165+/**
166166+ * Fallback chain for partial-translation locales.
167167+ * es-MX overrides a handful of Mexican-specific terms on top of the
168168+ * complete neutral Latin American base (es-419).
169169+ */
48170const FALLBACKS: Partial<Record<string, string>> = {
4949- 'es-419': 'es-MX',
171171+ 'es-MX': 'es-419',
50172}
5117352174const translations: Record<string, Record<string, string>> = {
53175 en,
54176 de,
55177 'es-MX': esMX,
178178+ 'es-ES': esES,
179179+ 'es-419': es419,
56180 fr,
181181+ it,
57182 ja,
58183 ko,
59184 'pt-BR': ptBR,
6060- 'es-419': es419,
185185+ tr,
61186}
62187188188+/** Returns `true` if `locale` is `"en"` or one of the supported non-default locales. */
63189export function isValidLocale(locale: string): boolean {
64190 return locale === DEFAULT_LOCALE || (SUPPORTED_LOCALES as readonly string[]).includes(locale)
65191}
66192193193+/**
194194+ * Returns a `t(key)` function scoped to `locale`.
195195+ * Lookup order: locale dict → fallback locale dict (e.g. es-MX → es-419) → English → key itself.
196196+ */
67197export function useTranslations(locale: string) {
68198 const dict = translations[locale] ?? {}
69199 const fallback = FALLBACKS[locale]
···77207 }
78208}
79209210210+/** Maps a locale string to the value for the HTML `lang` attribute (currently 1:1). */
80211export function localeToHtmlLang(locale: string): string {
81212 return locale
82213}
83214215215+/** Constructs a locale-prefixed URL path; English returns the bare path (no `/en/` prefix). */
84216export function getLocaleUrl(locale: string, path = '/'): string {
85217 const normalizedPath = path.startsWith('/') ? path : `/${path}`
86218 if (locale === DEFAULT_LOCALE) return normalizedPath
87219 return `/${locale}${normalizedPath === '/' ? '/' : normalizedPath}`
88220}
89221222222+/**
223223+ * Generates `<link rel="alternate" hreflang>` objects for a canonical path.
224224+ * Includes `x-default` and `en` pointing to the un-prefixed URL, plus one
225225+ * entry per supported locale at `/{locale}{canonicalPath}`.
226226+ */
90227export function hreflangLinks(canonicalPath: string): Array<{ hreflang: string; href: string }> {
91228 const base = 'https://getorbyt.com'
92229 const links = [
+25
src/middleware.ts
···3344const LOCALE_COOKIE = 'orbyt-locale'
5566+/**
77+ * Parses the `Accept-Language` header and returns the best-matching supported locale.
88+ * Tries an exact case-insensitive match first, then a base-language prefix match
99+ * (e.g. `de-AT` → `de`, `pt-PT` → `pt-BR`), then English variants, then defaults to `en`.
1010+ */
611function matchAcceptLanguage(header: string | null): string {
712 if (!header) return DEFAULT_LOCALE
813···3439 return DEFAULT_LOCALE
3540}
36414242+/**
4343+ * Astro middleware that resolves the active locale for every request and stores
4444+ * it in `context.locals.locale`. Also reads Cloudflare geo-IP (`cf.country` or
4545+ * `CF-IPCountry` header) into `context.locals.countryCode` for geo-aware flag display.
4646+ *
4747+ * Detection order:
4848+ * 1. `x-orbyt-locale` header set by an earlier internal rewrite (skips re-detection)
4949+ * 2. URL path prefix — `/de/`, `/ja/`, etc. (rewrites internally, strips prefix)
5050+ * 3. `orbyt-locale` cookie (persists user's manual language choice)
5151+ * 4. `Accept-Language` header
5252+ * 5. Default: `en`
5353+ *
5454+ * Non-English locales detected at the root path (`/`) are redirected to `/{locale}/`.
5555+ */
3756export const onRequest = defineMiddleware(async (context, next) => {
5757+ // Read Cloudflare geo-IP country — available via the cf object in Workers,
5858+ // or the CF-IPCountry header when running behind Cloudflare.
5959+ const cf = (context.locals as { runtime?: { cf?: { country?: string } } }).runtime?.cf
6060+ const rawCountry = cf?.country ?? context.request.headers.get('CF-IPCountry') ?? undefined
6161+ context.locals.countryCode = rawCountry?.toUpperCase()
6262+3863 // If this is a rewritten request from a locale-prefix path, skip re-detection
3964 const preResolved = context.request.headers.get('x-orbyt-locale')
4065 if (preResolved && isValidLocale(preResolved)) {
···88import { useTranslations, localeToHtmlLang } from '../i18n/utils';
991010const locale = Astro.locals.locale ?? 'en';
1111+const countryCode = Astro.locals.countryCode;
1112const t = useTranslations(locale);
1213---
1314<!DOCTYPE html>
···181182 <p>If you have questions about this Privacy Policy, visit our <a href="/contact">contact page</a>.</p>
182183 </main>
183184184184- <SiteFooter locale={locale} currentPath="/privacy" />
185185+ <SiteFooter locale={locale} currentPath="/privacy" countryCode={countryCode} />
185186 </div>
186187187188</body>
+2-1
src/pages/terms.astro
···88import { useTranslations, localeToHtmlLang } from '../i18n/utils';
991010const locale = Astro.locals.locale ?? 'en';
1111+const countryCode = Astro.locals.countryCode;
1112const t = useTranslations(locale);
1213---
1314<!DOCTYPE html>
···153154 <p>If you have any questions about these Terms or Orbyt, visit our <a href="/contact">contact page</a>.</p>
154155 </main>
155156156156- <SiteFooter locale={locale} currentPath="/terms" />
157157+ <SiteFooter locale={locale} currentPath="/terms" countryCode={countryCode} />
157158 </div>
158159159160</body>
+33-12
src/utils/bluesky-api.ts
···22const SITE_URL = 'https://getorbyt.com';
33const API_TIMEOUT = 5000;
4455+/** Normalized profile fields returned from `app.bsky.actor.getProfile`. */
56export interface ProfileData {
67 handle: string;
78 displayName?: string;
···1011 did: string;
1112}
12131414+/** Normalized post fields used by SSR profile and post pages. */
1315export interface PostData {
1416 text: string;
1517 createdAt?: string;
···8385 'User-Agent': 'Orbyt/1.0 (https://getorbyt.com)',
8486};
85878888+/** Wraps `fetch` with an `AbortController` timeout so hung API calls don't block SSR. */
8689async function fetchWithTimeout(url: string, timeout: number = API_TIMEOUT): Promise<Response> {
8790 const controller = new AbortController();
8891 const timeoutId = setTimeout(() => controller.abort(), timeout);
···9699 }
97100}
98101102102+/** Fetches a Bluesky profile by handle; returns `null` on any API or network error. */
99103export async function fetchProfile(handle: string): Promise<ProfileData | null> {
100104 try {
101105 const url = `${BLUESKY_API_BASE}/app.bsky.actor.getProfile?actor=${encodeURIComponent(handle)}`;
102106 const response = await fetchWithTimeout(url);
103103-107107+104108 if (!response.ok) return null;
105105-109109+106110 const data = (await response.json()) as BskyProfileResponse;
107111 if (data.error || !data.handle || !data.did) return null;
108112···118122 }
119123}
120124125125+/** Resolves an AT Protocol handle to its DID via `com.atproto.identity.resolveHandle`. */
121126async function resolveHandle(handle: string): Promise<string | null> {
122127 try {
123128 const url = `${BLUESKY_API_BASE}/com.atproto.identity.resolveHandle?handle=${encodeURIComponent(handle)}`;
124129 const response = await fetchWithTimeout(url);
125125-130130+126131 if (!response.ok) return null;
127127-132132+128133 const data = (await response.json()) as { did?: string };
129134 return data.did || null;
130135 } catch {
···132137 }
133138}
134139140140+/**
141141+ * Fetches a single post by handle + rkey (post ID).
142142+ * Resolves the handle to a DID first, then constructs the AT URI for `getPosts`.
143143+ * Handles plain video embeds and `recordWithMedia` wrappers; falls back to image thumbnails.
144144+ */
135145export async function fetchPost(handle: string, postId: string): Promise<PostData | null> {
136146 try {
137147 const did = await resolveHandle(handle);
138148 if (!did) return null;
139139-149149+140150 const atUri = `at://${did}/app.bsky.feed.post/${postId}`;
141151 const url = `${BLUESKY_API_BASE}/app.bsky.feed.getPosts?uris=${encodeURIComponent(atUri)}`;
142152 const response = await fetchWithTimeout(url);
143143-153153+144154 if (!response.ok) return null;
145145-155155+146156 const data = (await response.json()) as BskyGetPostsResponse;
147157 const posts = data.posts;
148158 if (!posts || posts.length === 0) return null;
···152162 let thumbnail: string | undefined;
153163 let videoUrl: string | undefined;
154164 let aspectRatio: { width: number; height: number } | undefined;
155155-165165+156166 if (post.embed) {
157167 if (post.embed.$type === 'app.bsky.embed.video#view') {
158168 thumbnail = post.embed.thumbnail;
···186196 thumbnail = post.embed.images[0].thumb || post.embed.images[0].fullsize;
187197 }
188198 }
189189-199199+190200 return {
191201 text: post.record?.text || '',
192202 createdAt: post.record?.createdAt,
···208218 }
209219}
210220221221+/** A minimal video post entry used for the infinite-scroll feed grid. */
211222export interface VideoPost {
212223 uri: string;
213224 postId: string;
···215226 caption: string;
216227}
217228229229+/** Paginated result from `fetchVideoPosts`. */
218230export interface VideoFeedResult {
219231 posts: VideoPost[];
220232 cursor: string | null;
221233}
222234235235+/** Extracts the rkey (last path segment) from an AT URI. */
223236function extractPostId(uri: string): string | null {
224237 if (!uri) return null;
225238 const parts = uri.split('/');
226239 return parts[parts.length - 1] || null;
227240}
228241242242+/** Truncates `text` to `maxLength` characters, appending `…` if cut. */
229243function truncateText(text: string, maxLength: number = 90): string {
230244 if (!text) return '';
231245 if (text.length <= maxLength) return text;
232246 return text.substring(0, maxLength).trim() + '...';
233247}
234248249249+/**
250250+ * Fetches the video-only feed for a handle using `posts_with_video` filter.
251251+ * Skips posts whose embed type is not `app.bsky.embed.video#view`.
252252+ * Returns an empty result (no throw) on any network or API error.
253253+ */
235254export async function fetchVideoPosts(handle: string, cursor?: string, limit: number = 30): Promise<VideoFeedResult> {
236255 try {
237256 let url = `${BLUESKY_API_BASE}/app.bsky.feed.getAuthorFeed?actor=${encodeURIComponent(handle)}&filter=posts_with_video&limit=${limit}`;
···240259 }
241260242261 const response = await fetchWithTimeout(url);
243243-262262+244263 if (!response.ok) {
245264 return { posts: [], cursor: null };
246265 }
···283302 }
284303}
285304305305+/** Ensures `url` is absolute using `SITE_URL` as the base; returns `fallback` when `url` is empty. */
286306export function toAbsoluteUrl(url: string | undefined, fallback: string = '/images/post/Orbyt-with-background.png'): string {
287307 if (!url) return `${SITE_URL}${fallback}`;
288308 if (url.startsWith('http://') || url.startsWith('https://')) return url;
···290310 return `${SITE_URL}/${url}`;
291311}
292312313313+/** Infers the image MIME type from URL path extension; defaults to `image/jpeg`. */
293314export function getImageMimeType(url: string): string {
294315 if (!url) return 'image/jpeg';
295295-316316+296317 const urlLower = url.toLowerCase();
297297-318318+298319 if (urlLower.includes('.png') || urlLower.endsWith('png')) {
299320 return 'image/png';
300321 }
+5
src/utils/ios-distribution.ts
···3434 'SE',
3535]);
36363737+/** Uppercases and validates a raw CF-IPCountry value; returns `null` for unknown/Tor (`XX`, `T1`) and malformed codes. */
3738function normalizeCountryCode(value: string | null | undefined): string | null {
3839 if (value == null || value === '') return null;
3940 const code = value.trim().toUpperCase();
···4142 return code;
4243}
43444545+/** Returns `true` for EU member states and Japan, where AltStore PAL is the primary iOS distribution channel. */
4446function isAltstorePalRegion(cfIpCountry: string | null | undefined): boolean {
4547 const code = normalizeCountryCode(cfIpCountry);
4648 if (code == null) return false;
···5961/** Same marketplace ID as `public/altstore/source.json` */
6062const DEFAULT_APP_STORE = 'https://apps.apple.com/app/id6751679299';
61636464+/** Builds iOS download options based on the visitor's country; AltStore PAL is primary for EU and Japan. */
6265export function getIosDownloadOptions(
6366 cfIpCountry: string | null | undefined,
6467 overrides?: Partial<Pick<IosDownloadOptions, 'altstoreSourceUrl' | 'appStoreUrl'>>,
···7477 };
7578}
76798080+/** Reads `cf-ipcountry` from the incoming request headers and delegates to `getIosDownloadOptions`. */
7781export function getIosDownloadOptionsFromRequest(
7882 request: Request,
7983 overrides?: Partial<Pick<IosDownloadOptions, 'altstoreSourceUrl' | 'appStoreUrl'>>,
···8993 return `altstore-pal://source?url=${encodeURIComponent(sourceUrl)}`;
9094}
91959696+/** Resolves the primary iOS download href from pre-built options (AltStore deep link or App Store URL). */
9297function iosHrefForOptions(options: IosDownloadOptions): string {
9398 return options.primary === 'altstore'
9499 ? altstorePalSourceDeepLink(options.altstoreSourceUrl)
+7
src/utils/orbyt-api.ts
···11+/** Profile color theme returned by the Orbyt color API. */
12export interface ColorData {
23 textColor: string;
34 backgroundColor: string;
···56 isBeta: boolean;
67}
7899+/** Minimal interface for the Cloudflare service binding to the `orbyt-api` Worker. */
810export interface OrbytApiBinding {
911 fetch(request: Request | string, init?: RequestInit): Promise<Response>;
1012}
11131414+/**
1515+ * Fetches profile color theme data for a DID from the Orbyt API.
1616+ * Tries targets in priority order: Cloudflare service binding → localhost (dev) → public API.
1717+ * Returns `null` if all targets fail or return a non-OK status.
1818+ */
1219export async function getColor(
1320 did: string,
1421 binding?: OrbytApiBinding
+2
src/utils/profile-colors.ts
···44 */
55export const ORBYT_BLACK = '#05070a' as const;
6677+/** Computes WCAG relative luminance for a 6-digit hex color using the IEC 61966-2-1 transfer function. */
78function getRelativeLuminance(hex: string): number {
89 const color = hex.replace('#', '');
910 const r = parseInt(color.substring(0, 2), 16) / 255;
···1415 return 0.2126 * transform(r) + 0.7152 * transform(g) + 0.0722 * transform(b);
1516}
16171818+/** Linear RGB blend of two 6-digit hex colors; `ratio` 0 = `color1`, 1 = `color2`. */
1719function blendColors(color1: string, color2: string, ratio: number): string {
1820 const r = Math.round(
1921 parseInt(color1.slice(1, 3), 16) * (1 - ratio) + parseInt(color2.slice(1, 3), 16) * ratio
+7
src/utils/richtext.ts
···11/** Mentions with a TLD link to /@handle; URLs become external links. */
2233+/** Escapes `&`, `<`, `>`, `"`, and `'` so user-provided text is safe to embed in HTML. */
34function escapeHtml(text: string): string {
45 return text
56 .replace(/&/g, '&')
···910 .replace(/'/g, ''');
1011}
11121313+/**
1414+ * Converts plain Bluesky post text to HTML, turning `@mention.bsky.social`
1515+ * handles into profile links and bare/www/TLD URLs into external anchor tags.
1616+ * Overlap between mentions and URLs is resolved in favour of the mention.
1717+ */
1218export function parseRichText(text: string, options?: {
1319 mentionClass?: string;
1420 linkClass?: string;
···125131 return result;
126132}
127133134134+/** Truncates `text` to `maxLength` characters (appending `…`) before calling `parseRichText`. */
128135export function parseRichTextTruncated(text: string, maxLength: number = 90, options?: {
129136 mentionClass?: string;
130137 linkClass?: string;
+17
src/utils/video-ambient-backdrop.ts
···22// Golden angle maximizes hue separation across consecutive hashes.
33const GOLDEN_ANGLE = 137.50776;
4455+/** Formats an oklch color triple as a CSS `oklch(...)` string. */
56function oklchToCss(l: number, c: number, h: number): string {
67 return `oklch(${l.toFixed(3)} ${c.toFixed(3)} ${h.toFixed(2)})`;
78}
891010+/** Wraps an arbitrary hue value into the [0, 360) range. */
911function normalizeHue(value: number): number {
1012 const hue = value % 360;
1113 return hue < 0 ? hue + 360 : hue;
1214}
13151616+/**
1717+ * Derives a deterministic pair of dark oklch colors from a seed URL.
1818+ * Primary and secondary hues are separated by ~150–210° so the gradient
1919+ * reads as two distinct tones without clashing.
2020+ */
1421function getVideoAmbientBackdropGradientColors(seedUrl: string | null): readonly [string, string] {
1522 const seed = seedUrl ?? 'fallback';
1623···3643const VIDEO_AMBIENT_BACKDROP_HIGHLIGHT_OPACITY = [0.07, 0.09, 0.11, 0.13] as const;
3744const VIDEO_AMBIENT_BACKDROP_SCRIM_OPACITY = [0.42, 0.46, 0.5, 0.54] as const;
38454646+/**
4747+ * Fast, non-cryptographic djb2-style hash for a string seed.
4848+ * Used to pick deterministic but visually varied backdrop parameters
4949+ * without any runtime randomness that would break SSR/hydration parity.
5050+ */
3951export function hashVideoAmbientBackdropSeed(value: string): number {
4052 let hash = 0;
4153 for (let i = 0; i < value.length; i += 1) {
···4557 return Math.abs(hash);
4658}
47596060+/**
6161+ * Builds a complete CSS inline style string for the video ambient backdrop element.
6262+ * All CSS custom properties (`--video-ambient-backdrop-*`) are derived from `seedUrl`
6363+ * so the same thumbnail URL always produces the same gradient, angle, and highlight.
6464+ */
4865export function getVideoAmbientBackdropInlineStyle(seedUrl: string | null): string {
4966 const seed = seedUrl ?? 'fallback';
5067 const [start, end] = getVideoAmbientBackdropGradientColors(seed);