added pubilc folder

This commit is contained in:
Hendrik Behme committed 2026-08-17 02:06:56 +02:00
1 parent 34f463071f
commit 7f6b100506
9 files changed
+1440 -1

No files matched your search

+1 -1
View File
@@ -1,4 +1,4 @@
.env
.env-frontend
import_files/
public
/backend/public
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

+183
View File
@@ -0,0 +1,183 @@
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
width="100%" viewBox="0 0 1024 1024" enable-background="new 0 0 1024 1024" xml:space="preserve">
<path fill="#000000" opacity="1.000000" stroke="none"
d="
M795.999878,657.367310
C760.167114,657.360779 724.834229,657.418213 689.501709,657.321106
C670.976624,657.270203 657.227661,646.780029 652.388062,629.050171
C651.357178,625.273621 651.202637,621.257812 650.576294,616.876770
C657.482727,616.035034 663.884888,616.073242 670.260620,616.588074
C670.547852,616.885864 670.906494,617.084229 670.980164,617.358948
C674.983643,632.287720 680.651123,636.650940 696.163025,636.652344
C765.328552,636.658569 834.494080,636.638977 903.659546,636.700073
C907.869934,636.703796 912.004822,636.176758 915.987915,635.146790
C924.782776,632.872986 930.628601,624.820740 930.633057,615.379333
C930.661804,554.880310 930.603699,494.381226 930.737244,433.882477
C930.747314,429.310455 929.062317,428.312653 924.881470,428.321350
C875.049133,428.425018 825.216614,428.379333 775.384094,428.375854
C743.051270,428.373596 710.718506,428.368530 678.385681,428.382355
C670.385254,428.385773 670.400513,428.502502 670.277771,436.467804
C670.044067,451.638245 672.689209,448.856934 657.284363,449.246796
C652.653748,449.363953 650.602417,447.342773 650.624512,442.681427
C650.706787,425.348785 650.489990,408.013336 650.713257,390.683197
C650.872620,378.314575 663.611938,366.842316 677.007446,366.687500
C687.672546,366.564270 698.341309,366.562439 709.005859,366.705078
C712.399292,366.750458 713.831848,365.694122 713.704346,362.137573
C713.495605,356.311951 713.589294,350.472198 713.675842,344.640198
C713.750366,339.620636 715.621033,337.715820 720.650818,337.661316
C728.649719,337.574646 736.651489,337.730652 744.649658,337.617584
C749.221863,337.552979 751.560669,339.780548 751.741394,344.142334
C751.968994,349.633331 752.067383,355.142029 751.899536,360.633209
C751.767700,364.944031 752.969666,366.804321 757.817566,366.772186
C787.315491,366.576630 816.815979,366.645477 846.314636,366.802856
C849.960205,366.822296 851.001709,365.435333 850.936890,362.134735
C850.835571,356.970978 850.838806,351.799805 850.993225,346.637634
C851.197388,339.809082 853.414307,337.677887 860.121277,337.643799
C867.120911,337.608246 874.121826,337.580017 881.120605,337.670807
C886.823242,337.744781 889.363220,340.217010 889.585815,345.834656
C889.776978,350.658508 889.892456,355.488403 889.898499,360.315796
C889.906494,366.693604 889.846558,366.694550 896.477905,366.689728
C905.644409,366.683044 914.816406,366.875366 923.976257,366.624054
C941.124512,366.153534 953.648743,379.861237 953.548584,395.994293
C953.094360,469.156952 953.355774,542.323975 953.346191,615.489441
C953.343018,639.938416 935.945862,657.346497 911.492554,657.359192
C873.161682,657.379028 834.830811,657.366089 795.999878,657.367310
z"/>
<path fill="#000000" opacity="1.000000" stroke="none"
d="
M528.409790,597.516602
C497.075287,620.785706 444.226227,612.023682 429.015320,560.438843
C421.740753,535.768616 424.170959,511.572845 439.374207,489.842865
C455.550232,466.722412 487.366638,456.664978 514.018005,465.788971
C521.454529,468.334839 527.421997,473.046326 533.668701,478.389252
C534.868774,475.810791 534.631958,473.367340 534.635498,471.001099
C534.661194,453.669586 534.632385,436.337952 534.665100,419.006439
C534.677490,412.450287 536.433655,410.689545 542.900940,410.657166
C550.733276,410.617950 558.566162,410.605804 566.398376,410.655701
C572.320129,410.693420 574.311279,412.552216 574.314453,418.325104
C574.347412,478.152313 574.348389,537.979553 574.320740,597.806763
C574.318115,603.473328 571.764221,606.091187 566.124390,606.208923
C559.794495,606.341064 553.459656,606.253235 547.127075,606.232300
C538.384033,606.203369 535.937500,604.023315 534.892944,595.424866
C534.815613,594.788635 534.606812,594.168396 534.276001,592.762329
C532.176392,594.468384 530.433411,595.884583 528.409790,597.516602
M536.676025,532.668274
C536.117737,518.525208 529.938965,507.667847 517.228394,501.240967
C504.691589,494.901886 492.378204,496.212128 480.863525,504.144287
C464.767395,515.232483 460.265320,540.059570 470.983459,557.949646
C483.140106,578.240845 509.116516,581.518616 525.439575,564.626892
C533.729370,556.048279 537.213196,545.494995 536.676025,532.668274
z"/>
<path fill="#000000" opacity="1.000000" stroke="none"
d="
M411.343933,595.812073
C411.222443,604.326233 409.331451,606.197693 401.241425,606.217773
C394.744415,606.233887 388.247101,606.251282 381.750366,606.206177
C373.682495,606.150146 371.660858,604.146240 371.656036,596.214417
C371.641754,572.725098 371.718536,549.235413 371.617584,525.746521
C371.573273,515.443726 368.031830,506.656311 358.522034,501.613953
C349.308990,496.728912 339.763672,497.080200 330.970001,502.664642
C322.013702,508.352356 317.891388,516.984375 317.918732,527.594727
C317.949219,539.422546 317.961517,551.250427 317.969666,563.078247
C317.977356,574.239746 318.019958,585.401611 317.951050,596.562683
C317.902039,604.504211 316.014832,606.340088 308.294373,606.344788
C301.130981,606.349121 293.966736,606.421570 286.804443,606.334900
C281.836975,606.274841 279.176941,603.693298 278.743622,598.748047
C278.627655,597.424377 278.685913,596.084290 278.685913,594.751770
C278.685486,555.436279 278.684845,516.120789 278.691925,476.805298
C278.693573,467.785950 279.816162,466.642700 288.617371,466.608826
C295.114166,466.583832 301.611633,466.617676 308.107697,466.537506
C316.139221,466.438385 316.798950,466.949768 318.007935,474.854065
C318.179169,475.973694 318.305328,477.100281 318.543365,478.919159
C321.681274,476.429321 324.312531,474.199677 327.085693,472.163269
C350.631165,454.873627 400.239380,460.166534 409.304962,504.100464
C410.547241,510.120819 411.408386,516.172302 411.384521,522.369873
C411.290894,546.692017 411.345367,571.014587 411.343933,595.812073
z"/>
<path fill="#000000" opacity="1.000000" stroke="none"
d="
M589.636841,534.999878
C589.640991,514.500916 589.623535,494.501953 589.661987,474.503082
C589.674622,467.917572 591.091431,466.563690 597.555847,466.562927
C604.555359,466.562073 611.555298,466.607178 618.554199,466.539062
C626.701172,466.459778 627.335083,466.959503 628.288025,474.915466
C628.406494,475.904846 628.405396,476.935425 629.628784,477.783325
C631.940063,477.073456 633.330383,474.878784 635.192688,473.375671
C642.799255,467.236206 651.144531,463.267914 661.215210,463.587219
C668.189880,463.808289 675.188538,463.284485 682.074890,464.986420
C701.912109,469.889099 717.578979,488.136353 719.345215,508.503510
C721.450684,532.781555 719.795044,557.109436 720.289246,581.409241
C720.401001,586.906616 720.365723,592.409302 720.281189,597.907776
C720.191223,603.753479 717.549438,606.322144 711.613586,606.361084
C703.947632,606.411316 696.280884,606.402100 688.614929,606.350281
C682.504333,606.308899 680.674316,604.455383 680.666870,598.192566
C680.638489,574.193787 680.665161,550.195068 680.654053,526.196289
C680.651428,520.446228 679.617065,514.889343 676.558716,509.954407
C666.466492,493.669189 642.606506,494.853424 632.738037,512.131897
C629.813416,517.252625 628.230652,522.707031 628.274780,528.778320
C628.443115,551.942566 628.346313,575.108765 628.326050,598.274231
C628.320496,604.633179 626.664551,606.328613 620.405457,606.357910
C612.739441,606.393738 605.072693,606.394409 597.406921,606.329346
C591.545837,606.279663 589.680054,604.416931 589.668030,598.496948
C589.625244,577.497986 589.642456,556.498962 589.636841,534.999878
z"/>
<path fill="#000000" opacity="1.000000" stroke="none"
d="
M849.782715,554.711548
C835.896301,577.190186 819.183838,596.970520 801.475647,616.661133
C795.724304,611.507751 791.442261,605.768860 786.949707,600.289978
C771.314697,581.222717 755.695068,562.068115 745.512817,539.339111
C735.929871,517.947571 735.117676,496.356903 747.230713,475.645508
C758.418396,456.516327 775.896423,447.060944 797.821411,445.672852
C813.106689,444.705078 827.347595,448.058899 839.726562,457.326752
C853.845337,467.897247 862.064209,482.018188 864.414978,499.640503
C867.123718,519.945801 860.191895,537.575745 849.782715,554.711548
M817.940430,533.498657
C832.330078,523.279175 835.928345,505.734222 826.468140,491.917389
C817.192444,478.369873 799.224121,474.699524 785.513489,483.551544
C771.904724,492.337830 767.687500,510.543579 776.234863,524.376282
C784.100098,537.105225 801.930237,543.450989 817.940430,533.498657
z"/>
<path fill="#000000" opacity="1.000000" stroke="none"
d="
M164.230560,579.098877
C161.430649,586.099060 158.689026,592.709106 156.132675,599.390076
C154.364395,604.011475 151.112350,606.271240 146.240265,606.285217
C137.410477,606.310486 128.580460,606.337769 119.750839,606.297485
C115.130913,606.276428 112.085350,604.244324 110.266777,599.720703
C93.696815,558.504150 76.976242,517.348206 60.334583,476.160461
C57.220692,468.453644 58.487259,466.663300 66.792244,466.646271
C75.622078,466.628174 84.452065,466.580109 93.281715,466.614502
C100.139725,466.641205 102.243279,468.248596 104.441467,474.944489
C112.995674,501.001221 121.532463,527.063721 130.106888,553.113770
C130.921417,555.588440 131.374649,558.248901 133.512802,560.729980
C134.596817,557.994141 135.651764,555.607788 136.499451,553.149902
C145.609146,526.736633 154.680695,500.310089 163.777176,473.892181
C165.401138,469.175842 168.337128,466.458221 173.742493,466.578461
C183.067520,466.785980 192.400955,466.579773 201.729828,466.663879
C207.445129,466.715363 208.961655,468.925934 206.816193,474.223358
C192.700943,509.075775 178.524414,543.903381 164.230560,579.098877
z"/>
<path fill="#000000" opacity="1.000000" stroke="none"
d="
M260.338562,485.006592
C260.337616,522.308533 260.340759,559.111755 260.332550,595.915039
C260.330627,604.575378 258.503235,606.372253 249.785645,606.374512
C242.957901,606.376221 236.129349,606.433655 229.302643,606.348267
C223.933762,606.281006 221.690506,604.126282 221.684235,598.741943
C221.635788,557.109314 221.635330,515.476685 221.680252,473.844055
C221.686081,468.440796 223.561905,466.677368 228.907120,466.593689
C236.564194,466.473724 244.222260,466.393372 251.880188,466.357727
C258.492737,466.326935 260.264771,468.059448 260.335876,474.516052
C260.372528,477.846313 260.339813,481.177277 260.338562,485.006592
z"/>
<path fill="#000000" opacity="1.000000" stroke="none"
d="
M220.625824,420.911560
C231.295700,405.947174 251.704590,405.947052 260.492920,420.572662
C265.935791,429.630768 264.519440,441.970642 257.211517,449.161285
C249.742859,456.510193 237.582672,457.861633 228.613968,452.339539
C219.633881,446.810394 215.476181,435.510529 218.742569,425.389709
C219.199707,423.973267 219.878220,422.628296 220.625824,420.911560
z"/>
</svg>

After

Width:  |  Height:  |  Size: 11 KiB

@@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8" ?>
<svg baseProfile="full" height="1024" version="1.1" width="1024" xmlns="http://www.w3.org/2000/svg" xmlns:ev="http://www.w3.org/2001/xml-events" xmlns:xlink="http://www.w3.org/1999/xlink"><defs /><path d="M 60,466 L 58,468 L 58,471 L 59,472 L 59,474 L 60,475 L 60,476 L 61,477 L 61,478 L 62,479 L 62,481 L 63,482 L 63,483 L 64,484 L 64,486 L 65,487 L 65,488 L 66,489 L 66,491 L 67,492 L 67,493 L 68,494 L 68,496 L 69,497 L 69,498 L 70,499 L 70,501 L 71,502 L 71,503 L 72,504 L 72,506 L 73,507 L 73,508 L 74,509 L 74,511 L 75,512 L 75,513 L 76,514 L 76,516 L 77,517 L 77,518 L 78,519 L 78,521 L 79,522 L 79,523 L 80,524 L 80,525 L 81,526 L 81,528 L 82,529 L 82,530 L 83,531 L 83,533 L 84,534 L 84,535 L 85,536 L 85,538 L 86,539 L 86,540 L 87,541 L 87,543 L 88,544 L 88,545 L 89,546 L 89,548 L 90,549 L 90,550 L 91,551 L 91,553 L 92,554 L 92,555 L 93,556 L 93,558 L 94,559 L 94,560 L 95,561 L 95,563 L 96,564 L 96,565 L 97,566 L 97,568 L 98,569 L 98,570 L 99,571 L 99,572 L 100,573 L 100,575 L 101,576 L 101,577 L 102,578 L 102,580 L 103,581 L 103,582 L 104,583 L 104,585 L 105,586 L 105,587 L 106,588 L 106,590 L 107,591 L 107,592 L 108,593 L 108,595 L 109,596 L 109,597 L 110,598 L 110,600 L 113,603 L 114,603 L 115,604 L 148,604 L 149,603 L 150,603 L 153,600 L 153,599 L 154,598 L 154,596 L 155,595 L 155,594 L 156,593 L 156,591 L 157,590 L 157,589 L 158,588 L 158,586 L 159,585 L 159,584 L 160,583 L 160,581 L 161,580 L 161,579 L 162,578 L 162,576 L 163,575 L 163,574 L 164,573 L 164,571 L 165,570 L 165,569 L 166,568 L 166,566 L 167,565 L 167,564 L 168,563 L 168,562 L 169,561 L 169,559 L 170,558 L 170,557 L 171,556 L 171,554 L 172,553 L 172,552 L 173,551 L 173,549 L 174,548 L 174,547 L 175,546 L 175,544 L 176,543 L 176,542 L 177,541 L 177,540 L 178,539 L 178,537 L 179,536 L 179,535 L 180,534 L 180,532 L 181,531 L 181,530 L 182,529 L 182,527 L 183,526 L 183,525 L 184,524 L 184,522 L 185,521 L 185,520 L 186,519 L 186,518 L 187,517 L 187,515 L 188,514 L 188,513 L 189,512 L 189,510 L 190,509 L 190,508 L 191,507 L 191,505 L 192,504 L 192,503 L 193,502 L 193,500 L 194,499 L 194,498 L 195,497 L 195,495 L 196,494 L 196,493 L 197,492 L 197,490 L 198,489 L 198,488 L 199,487 L 199,486 L 200,485 L 200,483 L 201,482 L 201,481 L 202,480 L 202,478 L 203,477 L 203,476 L 204,475 L 204,473 L 205,472 L 205,471 L 206,470 L 206,469 L 205,468 L 205,467 L 204,466 L 168,466 L 164,470 L 164,471 L 163,472 L 163,474 L 162,475 L 162,477 L 161,478 L 161,480 L 160,481 L 160,483 L 159,484 L 159,486 L 158,487 L 158,488 L 157,489 L 157,491 L 156,492 L 156,494 L 155,495 L 155,497 L 154,498 L 154,500 L 153,501 L 153,503 L 152,504 L 152,506 L 151,507 L 151,509 L 150,510 L 150,512 L 149,513 L 149,514 L 148,515 L 148,517 L 147,518 L 147,520 L 146,521 L 146,523 L 145,524 L 145,526 L 144,527 L 144,529 L 143,530 L 143,532 L 142,533 L 142,535 L 141,536 L 141,538 L 140,539 L 140,540 L 139,541 L 139,543 L 138,544 L 138,546 L 137,547 L 137,549 L 136,550 L 136,552 L 135,553 L 135,555 L 134,556 L 134,558 L 133,559 L 133,561 L 132,562 L 130,560 L 130,558 L 129,557 L 129,555 L 128,554 L 128,552 L 127,551 L 127,549 L 126,548 L 126,546 L 125,545 L 125,543 L 124,542 L 124,539 L 123,538 L 123,537 L 122,536 L 122,533 L 121,532 L 121,530 L 120,529 L 120,527 L 119,526 L 119,524 L 118,523 L 118,521 L 117,520 L 117,518 L 116,517 L 116,515 L 115,514 L 115,512 L 114,511 L 114,509 L 113,508 L 113,506 L 112,505 L 112,503 L 111,502 L 111,500 L 110,499 L 110,497 L 109,496 L 109,494 L 108,493 L 108,490 L 107,489 L 107,487 L 106,486 L 106,484 L 105,483 L 105,481 L 104,480 L 104,478 L 103,477 L 103,475 L 102,474 L 102,472 L 101,471 L 101,470 L 97,466 Z" fill="black" /><path d="M 253,465 L 252,466 L 223,466 L 222,467 L 222,468 L 221,469 L 221,600 L 222,601 L 222,602 L 224,604 L 254,604 L 255,603 L 256,603 L 257,602 L 257,601 L 258,600 L 258,469 L 255,466 L 254,466 Z" fill="black" /><path d="M 652,463 L 651,464 L 649,464 L 648,465 L 646,465 L 645,466 L 644,466 L 643,467 L 642,467 L 641,468 L 640,468 L 638,470 L 637,470 L 633,474 L 632,474 L 630,476 L 630,477 L 629,478 L 628,478 L 628,479 L 627,480 L 626,479 L 626,470 L 625,469 L 625,468 L 623,466 L 591,466 L 590,467 L 590,468 L 589,469 L 589,601 L 592,604 L 623,604 L 626,601 L 626,520 L 627,519 L 627,517 L 628,516 L 628,515 L 629,514 L 629,512 L 631,510 L 631,509 L 634,506 L 634,505 L 637,502 L 638,502 L 640,500 L 641,500 L 642,499 L 643,499 L 644,498 L 645,498 L 646,497 L 650,497 L 651,496 L 658,496 L 659,497 L 662,497 L 663,498 L 665,498 L 666,499 L 667,499 L 669,501 L 670,501 L 674,505 L 674,506 L 676,508 L 676,509 L 677,510 L 677,511 L 678,512 L 678,514 L 679,515 L 679,518 L 680,519 L 680,601 L 683,604 L 714,604 L 718,600 L 718,511 L 717,510 L 717,504 L 716,503 L 716,500 L 715,499 L 715,497 L 714,496 L 714,494 L 713,493 L 713,492 L 712,491 L 712,490 L 711,489 L 711,488 L 710,487 L 710,486 L 708,484 L 708,483 L 705,480 L 705,479 L 702,476 L 701,476 L 696,471 L 695,471 L 693,469 L 692,469 L 691,468 L 690,468 L 689,467 L 687,467 L 686,466 L 685,Line truncated

After

Width:  |  Height:  |  Size: 12 KiB

@@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8" ?>
<svg baseProfile="tiny" height="1024" version="1.2" width="1024" xmlns="http://www.w3.org/2000/svg" xmlns:ev="http://www.w3.org/2001/xml-events" xmlns:xlink="http://www.w3.org/1999/xlink"><defs /><text fill="black" font-family="Poppins" font-size="100px" x="10" y="130">vind</text><path d="M 60,466 L 58,468 L 58,471 L 59,472 L 59,474 L 60,475 L 60,476 L 61,477 L 61,478 L 62,479 L 62,481 L 63,482 L 63,483 L 64,484 L 64,486 L 65,487 L 65,488 L 66,489 L 66,491 L 67,492 L 67,493 L 68,494 L 68,496 L 69,497 L 69,498 L 70,499 L 70,501 L 71,502 L 71,503 L 72,504 L 72,506 L 73,507 L 73,508 L 74,509 L 74,511 L 75,512 L 75,513 L 76,514 L 76,516 L 77,517 L 77,518 L 78,519 L 78,521 L 79,522 L 79,523 L 80,524 L 80,525 L 81,526 L 81,528 L 82,529 L 82,530 L 83,531 L 83,533 L 84,534 L 84,535 L 85,536 L 85,538 L 86,539 L 86,540 L 87,541 L 87,543 L 88,544 L 88,545 L 89,546 L 89,548 L 90,549 L 90,550 L 91,551 L 91,553 L 92,554 L 92,555 L 93,556 L 93,558 L 94,559 L 94,560 L 95,561 L 95,563 L 96,564 L 96,565 L 97,566 L 97,568 L 98,569 L 98,570 L 99,571 L 99,572 L 100,573 L 100,575 L 101,576 L 101,577 L 102,578 L 102,580 L 103,581 L 103,582 L 104,583 L 104,585 L 105,586 L 105,587 L 106,588 L 106,590 L 107,591 L 107,592 L 108,593 L 108,595 L 109,596 L 109,597 L 110,598 L 110,600 L 113,603 L 114,603 L 115,604 L 148,604 L 149,603 L 150,603 L 153,600 L 153,599 L 154,598 L 154,596 L 155,595 L 155,594 L 156,593 L 156,591 L 157,590 L 157,589 L 158,588 L 158,586 L 159,585 L 159,584 L 160,583 L 160,581 L 161,580 L 161,579 L 162,578 L 162,576 L 163,575 L 163,574 L 164,573 L 164,571 L 165,570 L 165,569 L 166,568 L 166,566 L 167,565 L 167,564 L 168,563 L 168,562 L 169,561 L 169,559 L 170,558 L 170,557 L 171,556 L 171,554 L 172,553 L 172,552 L 173,551 L 173,549 L 174,548 L 174,547 L 175,546 L 175,544 L 176,543 L 176,542 L 177,541 L 177,540 L 178,539 L 178,537 L 179,536 L 179,535 L 180,534 L 180,532 L 181,531 L 181,530 L 182,529 L 182,527 L 183,526 L 183,525 L 184,524 L 184,522 L 185,521 L 185,520 L 186,519 L 186,518 L 187,517 L 187,515 L 188,514 L 188,513 L 189,512 L 189,510 L 190,509 L 190,508 L 191,507 L 191,505 L 192,504 L 192,503 L 193,502 L 193,500 L 194,499 L 194,498 L 195,497 L 195,495 L 196,494 L 196,493 L 197,492 L 197,490 L 198,489 L 198,488 L 199,487 L 199,486 L 200,485 L 200,483 L 201,482 L 201,481 L 202,480 L 202,478 L 203,477 L 203,476 L 204,475 L 204,473 L 205,472 L 205,471 L 206,470 L 206,469 L 205,468 L 205,467 L 204,466 L 168,466 L 164,470 L 164,471 L 163,472 L 163,474 L 162,475 L 162,477 L 161,478 L 161,480 L 160,481 L 160,483 L 159,484 L 159,486 L 158,487 L 158,488 L 157,489 L 157,491 L 156,492 L 156,494 L 155,495 L 155,497 L 154,498 L 154,500 L 153,501 L 153,503 L 152,504 L 152,506 L 151,507 L 151,509 L 150,510 L 150,512 L 149,513 L 149,514 L 148,515 L 148,517 L 147,518 L 147,520 L 146,521 L 146,523 L 145,524 L 145,526 L 144,527 L 144,529 L 143,530 L 143,532 L 142,533 L 142,535 L 141,536 L 141,538 L 140,539 L 140,540 L 139,541 L 139,543 L 138,544 L 138,546 L 137,547 L 137,549 L 136,550 L 136,552 L 135,553 L 135,555 L 134,556 L 134,558 L 133,559 L 133,561 L 132,562 L 130,560 L 130,558 L 129,557 L 129,555 L 128,554 L 128,552 L 127,551 L 127,549 L 126,548 L 126,546 L 125,545 L 125,543 L 124,542 L 124,539 L 123,538 L 123,537 L 122,536 L 122,533 L 121,532 L 121,530 L 120,529 L 120,527 L 119,526 L 119,524 L 118,523 L 118,521 L 117,520 L 117,518 L 116,517 L 116,515 L 115,514 L 115,512 L 114,511 L 114,509 L 113,508 L 113,506 L 112,505 L 112,503 L 111,502 L 111,500 L 110,499 L 110,497 L 109,496 L 109,494 L 108,493 L 108,490 L 107,489 L 107,487 L 106,486 L 106,484 L 105,483 L 105,481 L 104,480 L 104,478 L 103,477 L 103,475 L 102,474 L 102,472 L 101,471 L 101,470 L 97,466 Z" fill="black" /><path d="M 253,465 L 252,466 L 223,466 L 222,467 L 222,468 L 221,469 L 221,600 L 222,601 L 222,602 L 224,604 L 254,604 L 255,603 L 256,603 L 257,602 L 257,601 L 258,600 L 258,469 L 255,466 L 254,466 Z" fill="black" /><path d="M 652,463 L 651,464 L 649,464 L 648,465 L 646,465 L 645,466 L 644,466 L 643,467 L 642,467 L 641,468 L 640,468 L 638,470 L 637,470 L 633,474 L 632,474 L 630,476 L 630,477 L 629,478 L 628,478 L 628,479 L 627,480 L 626,479 L 626,470 L 625,469 L 625,468 L 623,466 L 591,466 L 590,467 L 590,468 L 589,469 L 589,601 L 592,604 L 623,604 L 626,601 L 626,520 L 627,519 L 627,517 L 628,516 L 628,515 L 629,514 L 629,512 L 631,510 L 631,509 L 634,506 L 634,505 L 637,502 L 638,502 L 640,500 L 641,500 L 642,499 L 643,499 L 644,498 L 645,498 L 646,497 L 650,497 L 651,496 L 658,496 L 659,497 L 662,497 L 663,498 L 665,498 L 666,499 L 667,499 L 669,501 L 670,501 L 674,505 L 674,506 L 676,508 L 676,509 L 677,510 L 677,511 L 678,512 L 678,514 L 679,515 L 679,518 L 680,519 L 680,601 L 683,604 L 714,604 L 718,600 L 718,511 L 717,510 L 717,504 L 716,503 L 716,500 L 715,499 L 715,497 L 714,496 L 714,494 L 713,493 L 713,492 L 712,491 L 712,490 L 711,489 L 711,488 L 710,487 L 710,486 L 708,484 L 708,483 L 705,480 L 705,479 L 702,476 L 701,476 L 696,471 LLine truncated

After

Width:  |  Height:  |  Size: 12 KiB

+9
View File
@@ -0,0 +1,9 @@
<?php
use App\Kernel;
require_once dirname(__DIR__).'/vendor/autoload_runtime.php';
return function (array $context) {
return new Kernel($context['APP_ENV'], (bool) $context['APP_DEBUG']);
};
+762
View File
@@ -0,0 +1,762 @@
(function (window) {
// The leaflet-gesture-handling UMD build exposes its class as
// window.leafletGestureHandling.GestureHandling instead of registering
// itself on L (unlike the other Leaflet plugins loaded on this page), so
// it has to be wired in manually before any L.map() call.
if (window.L && window.leafletGestureHandling && !window.L.Map.prototype.options.gestureHandling) {
window.L.Map.addInitHook('addHandler', 'gestureHandling', window.leafletGestureHandling.GestureHandling);
}
// Köln — last-resort fallback if a caller doesn't pass its own center;
// both controllers (Home/Map) explicitly pass the same point themselves,
// so this only matters if that ever gets forgotten.
var DEFAULT_CENTER = [50.9375, 6.9603];
// Leaflet.awesome-markers only ships a fixed palette of named colors (no
// arbitrary hex/hsl) — curated to the ones that read well against the
// grayscale-filtered basemap. Approximate hex values below are only used
// for the legend swatches, so they match what's actually drawn on pins.
var AWESOME_COLORS = ['red', 'darkred', 'orange', 'green', 'darkgreen', 'blue', 'darkblue', 'lightblue', 'purple', 'darkpurple', 'cadetblue', 'beige'];
var AWESOME_COLOR_HEX = {
red: '#d63e2a', darkred: '#a23336', orange: '#f69730', green: '#72b026',
darkgreen: '#728224', blue: '#38aadd', darkblue: '#0067a3', lightblue: '#8adaff',
purple: '#d252b9', darkpurple: '#5b396b', cadetblue: '#436978', beige: '#e0a458'
};
// Real ODH category names are German tourism-taxonomy strings (80+ of
// them) — this maps keywords found in a name to a Font Awesome 4 glyph,
// first match wins, falling back to a generic calendar icon.
var CATEGORY_ICON_RULES = [
{ icon: 'tree', words: ['weihnacht'] },
{ icon: 'music', words: ['musik', 'konzert', 'jazz', 'rock', 'pop', 'blues', 'soul', 'reggae', 'techno', 'house', 'elektro', 'punk', 'metal', 'folk', 'country', 'swing', 'gospel', 'klassisch', 'oper', 'musical', 'schlager', 'sakralmusik', 'volksmusik', 'dancehall', 'r\'n\'b'] },
{ icon: 'comments', words: ['theater', 'bühne', 'schauspiel', 'kabarett', 'ballett', 'figurentheater'] },
{ icon: 'film', words: ['film'] },
{ icon: 'shopping-cart', words: ['markt', 'flohmarkt', 'messe'] },
{ icon: 'flag', words: ['festival', 'fest', 'kirmes', 'jahrmarkt', 'stadtfest'] },
{ icon: 'binoculars', words: ['führung', 'besichtigung', 'ausflug', 'exkursion'] },
{ icon: 'bicycle', words: ['radtour', 'rad'] },
{ icon: 'trophy', words: ['sport', 'turnier', 'wettbewerb', 'motorsport'] },
{ icon: 'map-signs', words: ['wanderung'] },
{ icon: 'cutlery', words: ['gastronomie', 'genuss', 'gourmet'] },
{ icon: 'child', words: ['kinder', 'familie'] },
{ icon: 'heartbeat', words: ['gesundheit', 'wellness'] },
{ icon: 'university', words: ['kirche', 'sakral'] },
{ icon: 'book', words: ['vortrag', 'lesung', 'kurs', 'seminar', 'workshop', 'hobby'] },
{ icon: 'users', words: ['kongress', 'geselligkeit', 'spiele', 'treffen'] },
{ icon: 'glass', words: ['party', 'nightlife'] },
{ icon: 'picture-o', words: ['ausstellung'] }
];
var DEFAULT_ICON = 'calendar';
function hashHue(str) {
var hash = 0;
for (var i = 0; i < str.length; i++) {
hash = (hash * 31 + str.charCodeAt(i)) >>> 0;
}
return hash;
}
function categoryColor(name) {
if (!name) return 'hsl(220, 10%, 55%)';
return 'hsl(' + (hashHue(name) % 360) + ', 62%, 42%)';
}
function categoryMarkerColorName(name) {
if (!name) return 'cadetblue';
return AWESOME_COLORS[hashHue(name) % AWESOME_COLORS.length];
}
function categoryMarkerHex(name) {
return AWESOME_COLOR_HEX[categoryMarkerColorName(name)] || '#436978';
}
function categoryIconName(name) {
var lower = (name || '').toLowerCase();
for (var i = 0; i < CATEGORY_ICON_RULES.length; i++) {
var rule = CATEGORY_ICON_RULES[i];
for (var j = 0; j < rule.words.length; j++) {
if (lower.indexOf(rule.words[j]) !== -1) return rule.icon;
}
}
return DEFAULT_ICON;
}
function ensurePulseStyle() {
if (document.getElementById('vindn-pulse-style')) return;
var styleTag = document.createElement('style');
styleTag.id = 'vindn-pulse-style';
styleTag.textContent = '@keyframes vindnPulse{0%{transform:scale(.6);opacity:.45}100%{transform:scale(2.4);opacity:0}}';
document.head.appendChild(styleTag);
}
// Plain pulsing dot — used only for the "you are here" user-location
// marker, kept visually distinct from the category pins below.
function dotIcon(color, big) {
var size = big ? 18 : 14;
var ring = big
? '<span style="position:absolute;inset:-8px;border-radius:50%;background:' + color + ';opacity:.25;animation:vindnPulse 2.2s ease-out infinite;"></span>'
: '';
return L.divIcon({
className: 'pin-icon',
html: '<span style="position:relative;display:block;width:' + size + 'px;height:' + size + 'px;">' + ring +
'<span style="position:absolute;inset:0;border-radius:50%;background:' + color + ';border:2px solid #fff;box-shadow:0 1px 4px rgba(0,0,0,.35);"></span></span>',
iconSize: [size, size],
iconAnchor: [size / 2, size / 2]
});
}
// Category pin — Font Awesome glyph in a colored teardrop marker, falling
// back to a plain dot if the awesome-markers plugin didn't load.
function categoryIcon(name) {
if (window.L.AwesomeMarkers) {
return L.AwesomeMarkers.icon({
icon: categoryIconName(name),
prefix: 'fa',
markerColor: categoryMarkerColorName(name)
});
}
return dotIcon(categoryColor(name), false);
}
// OpenFreeMap's "liberty" style labels everything with
// coalesce(name_en, name) — English first — which shows "Cologne"
// instead of "Köln" even though the whole map (and every event on it)
// is German. Rewrites that one recurring sub-expression, wherever it
// appears, to prefer name_de then the OSM-local `name` (which for
// German places already *is* the German name) before falling back to
// English. Layers that don't contain it (road-shield ref numbers, etc.)
// are left alone.
function preferGermanLabels(maplibreMap) {
var EN_FIRST = '["coalesce",["get","name_en"],["get","name"]]';
var DE_FIRST = '["coalesce",["get","name_de"],["get","name"],["get","name_en"]]';
var style = maplibreMap.getStyle();
if (!style || !style.layers) return;
style.layers.forEach(function (layer) {
var textField = layer.layout && layer.layout['text-field'];
if (!textField) return;
var serialized = JSON.stringify(textField);
if (serialized.indexOf(EN_FIRST) === -1) return;
maplibreMap.setLayoutProperty(layer.id, 'text-field', JSON.parse(serialized.split(EN_FIRST).join(DE_FIRST)));
});
}
function readEmbeddedJson(elementId) {
var el = document.getElementById(elementId);
if (!el) return [];
try {
return JSON.parse(el.textContent) || [];
} catch (e) {
return [];
}
}
function toRad(d) { return d * Math.PI / 180; }
function distanceKm(lat1, lon1, lat2, lon2) {
var R = 6371;
var dLat = toRad(lat2 - lat1);
var dLon = toRad(lon2 - lon1);
var a = Math.sin(dLat / 2) * Math.sin(dLat / 2) +
Math.cos(toRad(lat1)) * Math.cos(toRad(lat2)) * Math.sin(dLon / 2) * Math.sin(dLon / 2);
return R * 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
}
// Returns the `count` events closest to `center`, each tagged with `_dist` (km).
function nearest(center, events, count) {
return events
.map(function (ev) {
ev._dist = distanceKm(center[0], center[1], ev.lat, ev.lon);
return ev;
})
.sort(function (a, b) { return a._dist - b._dist; })
.slice(0, count);
}
// Builds an (unattached) marker for an event — caller adds it to either
// the map directly or a MarkerClusterGroup.
function buildMarker(ev, onMarkerClick) {
var hex = categoryMarkerHex(ev.category);
var marker = L.marker([ev.lat, ev.lon], { icon: categoryIcon(ev.category) });
var popupHtml = '<div class="pin-pop">' +
(ev.category ? '<span class="cat" style="color:' + hex + ';">' + ev.category + '</span>' : '') +
'<h4>' + ev.title + '</h4>' +
(ev.venue ? '<p>' + ev.venue + '</p>' : '') +
(ev.url ? '<a href="' + ev.url + '">View event →</a>' : '') +
'</div>';
marker.bindPopup(popupHtml);
if (onMarkerClick) marker.on('click', function () { onMarkerClick(ev, marker); });
return marker;
}
function fitToMarkers(map, markers) {
var list = Object.keys(markers).map(function (id) { return markers[id]; });
if (!list.length) return;
if (list.length === 1) { map.setView(list[0].getLatLng(), 13); return; }
map.fitBounds(L.featureGroup(list).getBounds(), { padding: [40, 40], maxZoom: 14, animate: false });
}
// Renders a Leaflet map of category pins (one per event) into
// containerId, clustered via Leaflet.markercluster when it's loaded.
// events: [{id, title, venue, category, lat, lon, url}]
function renderPinMap(containerId, events, opts) {
opts = opts || {};
var el = document.getElementById(containerId);
if (!el || !window.L) return null;
ensurePulseStyle();
var center = opts.center || (events.length ? [events[0].lat, events[0].lon] : DEFAULT_CENTER);
var map = L.map(containerId, {
zoomControl: !!opts.zoomControl,
attributionControl: !!opts.attributionControl,
scrollWheelZoom: !!opts.scrollWheelZoom,
// A raster L.tileLayer used to supply maxZoom implicitly; the
// MapLibre GL bridge doesn't. minZoom/maxBounds are maplibre-gl-
// leaflet's own documented recommendation, to avoid Leaflet/MapLibre
// disagreeing about zoom 0 and latitudes MapLibre doesn't support.
minZoom: 1,
maxZoom: 19,
maxBounds: [[-90, -Infinity], [90, Infinity]],
maxBoundsViscosity: 1,
// Requires two-finger drag / Ctrl+scroll to pan/zoom, with an
// on-map hint, so an embedded map (home hero, mid-page /map) doesn't
// trap page scroll/pinch-zoom on touch devices.
gestureHandling: opts.gestureHandling !== false
}).setView(center, opts.zoom || 13);
if (L.maplibreGL) {
// OpenFreeMap serves vector tiles only, rendered by MapLibre GL and
// bridged into this Leaflet map as an ordinary layer — everything
// else (markers, clustering, fullscreen) is untouched by this.
var glLayer = L.maplibreGL({
style: 'https://tiles.openfreemap.org/styles/liberty',
attribution: '&copy; <a href="https://openfreemap.org" target="_blank">OpenFreeMap</a> <a href="https://www.openmaptiles.org/" target="_blank">© OpenMapTiles</a> Data from <a href="https://www.openstreetmap.org/copyright" target="_blank">OpenStreetMap</a>'
}).addTo(map);
var glMap = glLayer.getMaplibreMap && glLayer.getMaplibreMap();
if (glMap) {
// 'load' waits for full tile loading and can take a while;
// 'styledata' fires as soon as the style/layers themselves are
// parsed, which is all setLayoutProperty needs.
glMap.once('styledata', function () { preferGermanLabels(glMap); });
}
} else {
// Fallback if the MapLibre bridge failed to load for some reason.
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
maxZoom: 18,
attribution: opts.attributionControl ? '&copy; OpenStreetMap contributors' : ''
}).addTo(map);
}
if (opts.fullscreenControl !== false && L.control && L.control.fullscreen) {
// Fullscreen the whole .stage (search bar, locate button, panel), not
// just the bare map div — those overlay controls are DOM siblings of
// #mapCanvas, not descendants, so they'd vanish in fullscreen otherwise.
L.control.fullscreen({
title: 'View fullscreen',
titleCancel: 'Exit fullscreen',
fullscreenElement: el.closest('.stage') || el
}).addTo(map);
// Fullscreen means the map owns the whole viewport, so the two-
// finger/Ctrl+scroll gating gesture handling exists for (avoiding
// trapped page scroll) no longer applies — switch back to normal
// single-finger/scroll interaction while fullscreen is active.
if (map.gestureHandling) {
map.on('enterFullscreen', function () { map.gestureHandling.disable(); });
map.on('exitFullscreen', function () { map.gestureHandling.enable(); });
}
}
var clusterGroup = (opts.cluster !== false && L.markerClusterGroup)
// animate:false — a fitBounds() called right after the initial
// addLayer() (see fitToMarkers) otherwise races the cluster expand/
// collapse animation and leaves stale icon elements in the DOM.
? L.markerClusterGroup({ maxClusterRadius: 50, spiderfyOnMaxZoom: true, showCoverageOnHover: false, animate: false })
: null;
if (clusterGroup) map.addLayer(clusterGroup);
var markers = {};
events.forEach(function (ev) {
var marker = buildMarker(ev, opts.onMarkerClick);
markers[ev.id] = marker;
if (clusterGroup) clusterGroup.addLayer(marker);
else marker.addTo(map);
});
return { map: map, markers: markers, clusterGroup: clusterGroup };
}
/**
* Wires up the full interactive map "stage" used on both the home page
* and /map: pin map, category chips (#catChips), legend (#legend),
* nearby-events panel (#panelList/#resultCount), and a "use my location"
* button (#locateBtn). All element ids are optional — missing
* ones are just skipped, so the same call works whether or not a given
* piece of UI is present on the page.
*
* opts.limit turns on the "light" mode used by the home page: instead of
* plotting every event, it shows only the `limit` nearest to a default
* center (opts.defaultCenter) until the user shares their real location,
* at which point it re-picks the `limit` nearest to *that* from the full
* event list and refits the map to them.
*/
function initMapStage(opts) {
opts = opts || {};
var allEvents = opts.events || [];
var limit = opts.limit || null;
var defaultCenter = opts.defaultCenter || DEFAULT_CENTER;
var containerId = opts.containerId || 'mapCanvas';
var state = { cat: 'all', userLoc: null };
var shown = limit ? nearest(defaultCenter, allEvents.slice(), limit) : allEvents;
var result = renderPinMap(containerId, shown, {
zoomControl: true,
attributionControl: true,
zoom: opts.zoom || (limit ? 11 : (allEvents.length ? 9 : 7)),
center: limit ? defaultCenter : undefined,
onMarkerClick: function (ev) { highlightRow(ev.id); }
});
var map = result ? result.map : null;
var markers = result ? result.markers : {};
var clusterGroup = result ? result.clusterGroup : null;
var userMarker = null;
if (limit && map) fitToMarkers(map, markers);
var legendEl = document.getElementById('legend');
if (legendEl) {
var seenCats = [];
allEvents.forEach(function (ev) {
if (ev.category && seenCats.indexOf(ev.category) === -1) seenCats.push(ev.category);
});
legendEl.innerHTML = seenCats.slice(0, 8).map(function (cat) {
return '<span><span class="dot" style="background:' + categoryMarkerHex(cat) + ';"></span>' + cat + '</span>';
}).join('');
}
function addMarker(marker) {
if (clusterGroup) clusterGroup.addLayer(marker);
else if (map) marker.addTo(map);
}
function removeMarker(marker) {
if (clusterGroup) clusterGroup.removeLayer(marker);
else if (map) map.removeLayer(marker);
}
function rebuildMarkers(list) {
Object.keys(markers).forEach(function (id) { removeMarker(markers[id]); });
markers = {};
list.forEach(function (ev) {
var marker = buildMarker(ev, function (clicked) { highlightRow(clicked.id); });
markers[ev.id] = marker;
addMarker(marker);
});
shown = list;
}
function highlightRow(id) {
document.querySelectorAll('.map-row').forEach(function (r) { r.classList.remove('is-highlighted'); });
var row = document.querySelector('.map-row[data-id="' + id + '"]');
if (row) { row.classList.add('is-highlighted'); row.scrollIntoView({ block: 'nearest', behavior: 'smooth' }); }
}
function currentList() {
if (limit) {
var list = shown.slice();
if (state.userLoc) {
list = list.map(function (ev) { ev._dist = distanceKm(state.userLoc[0], state.userLoc[1], ev.lat, ev.lon); return ev; })
.sort(function (a, b) { return a._dist - b._dist; });
}
return list;
}
var visible = allEvents.filter(function (ev) { return state.cat === 'all' || ev.category === state.cat; });
if (state.userLoc) {
visible = visible.map(function (ev) { ev._dist = distanceKm(state.userLoc[0], state.userLoc[1], ev.lat, ev.lon); return ev; })
.sort(function (a, b) { return a._dist - b._dist; });
}
Object.keys(markers).forEach(function (id) {
var isVisible = visible.some(function (ev) { return String(ev.id) === String(id); });
var m = markers[id];
var hasLayer = clusterGroup ? clusterGroup.hasLayer(m) : (map && map.hasLayer(m));
if (isVisible && !hasLayer) addMarker(m);
if (!isVisible && hasLayer) removeMarker(m);
});
return visible;
}
function renderList() {
var list = currentList();
var countEl = document.getElementById('resultCount');
if (countEl) countEl.textContent = list.length + (list.length === 1 ? ' event' : ' events');
var panelEl = document.getElementById('panelList');
if (!panelEl) return;
panelEl.innerHTML = list.map(function (ev) {
var distHtml = '';
if (ev._dist != null) {
var distLabel = ev._dist < 1 ? Math.round(ev._dist * 1000) : ev._dist.toFixed(1);
var distUnit = ev._dist < 1 ? 'm' : 'km';
distHtml = '<div class="dist">' + distLabel + '<span class="u">' + distUnit + '</span></div>';
}
return (
'<div class="map-row' + (ev._dist != null ? '' : ' no-dist') + '" data-id="' + ev.id + '" tabindex="0" role="button" aria-label="' + ev.title + '">' +
distHtml +
'<div><div class="name">' + ev.title + '</div><span class="when">' + (ev.venue || '') + '</span></div>' +
'<div class="go">→</div>' +
'</div>'
);
}).join('');
panelEl.querySelectorAll('.map-row').forEach(function (row) {
row.addEventListener('click', function () {
var id = row.getAttribute('data-id');
var ev = allEvents.filter(function (e) { return String(e.id) === String(id); })[0];
if (!ev || !map) return;
map.flyTo([ev.lat, ev.lon], 15);
var marker = markers[id];
if (marker) {
if (clusterGroup && clusterGroup.zoomToShowLayer) {
clusterGroup.zoomToShowLayer(marker, function () { marker.openPopup(); });
} else {
marker.openPopup();
}
}
highlightRow(id);
});
});
}
var chipsEl = document.getElementById('catChips');
if (chipsEl) {
chipsEl.addEventListener('click', function (e) {
var btn = e.target.closest('.chip-sm');
if (!btn) return;
chipsEl.querySelectorAll('.chip-sm').forEach(function (c) { c.classList.remove('is-active'); });
btn.classList.add('is-active');
state.cat = btn.getAttribute('data-cat');
renderList();
});
}
function requestLocation() {
var statusEl = document.getElementById('locateStatus');
if (!navigator.geolocation) {
if (statusEl) statusEl.textContent = 'Location not supported by your browser';
return;
}
if (statusEl) statusEl.textContent = 'Finding you…';
navigator.geolocation.getCurrentPosition(function (pos) {
var lat = pos.coords.latitude;
var lon = pos.coords.longitude;
state.userLoc = [lat, lon];
if (limit) {
rebuildMarkers(nearest([lat, lon], allEvents.slice(), limit));
if (statusEl) statusEl.textContent = 'Showing the ' + limit + ' nearest events';
} else if (statusEl) {
statusEl.textContent = 'Showing events near you';
}
if (map) {
if (userMarker) map.removeLayer(userMarker);
userMarker = L.marker([lat, lon], { icon: dotIcon('#2440FF', true), zIndexOffset: 1000 }).addTo(map);
if (limit) fitToMarkers(map, markers);
else map.setView([lat, lon], 13);
}
renderList();
}, function () {
if (statusEl) statusEl.textContent = "Couldn't get your location";
}, { timeout: 8000 });
}
var locateBtn = document.getElementById('locateBtn');
if (locateBtn) locateBtn.addEventListener('click', requestLocation);
// If the user already granted location access (on this or an earlier
// visit), center on it automatically — this only *reads* the current
// permission state, it never triggers the browser's permission prompt
// itself, so it's safe to check unconditionally on load.
if (navigator.permissions && navigator.permissions.query) {
navigator.permissions.query({ name: 'geolocation' }).then(function (status) {
if (status.state === 'granted') requestLocation();
}).catch(function () {});
}
renderList();
return { map: map, markers: markers };
}
/**
* The /map page's full radius-based explorer — replaces a flat "first
* N events" cap with "events within rangeKm of a source point", where
* the source starts at the server-resolved center (Köln by default, or
* ?lat/&lon) and can move via a map click/marker drag, a place search
* (Nominatim), or "use my location". Assumes the full (non-compact)
* _map_stage.html.twig markup — #catChips, #legend, #rangeSelect,
* #locationSearchForm all present.
*
* opts.events is the full pool the server already limited to
* opts.maxRangeKm around opts.center; moving the source further than
* that away can't be served from the pool, so it triggers a fresh page
* load centered on the new point instead of silently showing partial
* results.
*/
function initRadiusMapStage(opts) {
opts = opts || {};
var allEvents = opts.events || [];
var originalCenter = opts.center || DEFAULT_CENTER;
var maxRangeKm = opts.maxRangeKm || 100;
var containerId = opts.containerId || 'mapCanvas';
var state = { source: originalCenter.slice(), rangeKm: opts.rangeKm || 25, cat: 'all' };
var result = renderPinMap(containerId, [], {
zoomControl: true,
attributionControl: true,
center: state.source,
zoom: opts.zoom || 11,
// This map already owns the whole viewport (no surrounding page to
// protect from trapped scroll/pinch), so gesture handling would only
// make one-finger dragging feel unresponsive for no benefit.
gestureHandling: false,
onMarkerClick: function (ev) { highlightRow(ev.id); }
});
var map = result ? result.map : null;
var markers = result ? result.markers : {};
var clusterGroup = result ? result.clusterGroup : null;
var sourceMarker = map
? L.marker(state.source, { icon: dotIcon('#14140F', true), draggable: true, zIndexOffset: 1000 }).addTo(map)
: null;
function addMarker(marker) {
if (clusterGroup) clusterGroup.addLayer(marker);
else if (map) marker.addTo(map);
}
function removeMarker(marker) {
if (clusterGroup) clusterGroup.removeLayer(marker);
else if (map) map.removeLayer(marker);
}
function rebuildMarkers(list) {
Object.keys(markers).forEach(function (id) { removeMarker(markers[id]); });
markers = {};
list.forEach(function (ev) {
var marker = buildMarker(ev, function (clicked) { highlightRow(clicked.id); });
markers[ev.id] = marker;
addMarker(marker);
});
}
function highlightRow(id) {
document.querySelectorAll('.map-row').forEach(function (r) { r.classList.remove('is-highlighted'); });
var row = document.querySelector('.map-row[data-id="' + id + '"]');
if (row) { row.classList.add('is-highlighted'); row.scrollIntoView({ block: 'nearest', behavior: 'smooth' }); }
}
function inRange() {
return allEvents
.filter(function (ev) { return state.cat === 'all' || ev.category === state.cat; })
.map(function (ev) { ev._dist = distanceKm(state.source[0], state.source[1], ev.lat, ev.lon); return ev; })
.filter(function (ev) { return ev._dist <= state.rangeKm; })
.sort(function (a, b) { return a._dist - b._dist; });
}
function updateUrl() {
if (!window.history || !window.history.replaceState || !window.URL) return;
var url = new URL(window.location.href);
url.searchParams.set('lat', state.source[0].toFixed(5));
url.searchParams.set('lon', state.source[1].toFixed(5));
url.searchParams.set('range', state.rangeKm);
window.history.replaceState(null, '', url.toString());
}
function render() {
var visible = inRange();
rebuildMarkers(visible);
var legendEl = document.getElementById('legend');
if (legendEl) {
var seenCats = [];
visible.forEach(function (ev) {
if (ev.category && seenCats.indexOf(ev.category) === -1) seenCats.push(ev.category);
});
legendEl.innerHTML = seenCats.slice(0, 8).map(function (cat) {
return '<span><span class="dot" style="background:' + categoryMarkerHex(cat) + ';"></span>' + cat + '</span>';
}).join('');
}
var statusEl = document.getElementById('locateStatus');
if (statusEl) statusEl.textContent = 'Showing events within ' + state.rangeKm + ' km';
var countEl = document.getElementById('resultCount');
if (countEl) countEl.textContent = visible.length + (visible.length === 1 ? ' event' : ' events');
var panelEl = document.getElementById('panelList');
if (panelEl) {
panelEl.innerHTML = visible.map(function (ev) {
var distLabel = ev._dist < 1 ? Math.round(ev._dist * 1000) : ev._dist.toFixed(1);
var distUnit = ev._dist < 1 ? 'm' : 'km';
return (
'<div class="map-row" data-id="' + ev.id + '" tabindex="0" role="button" aria-label="' + ev.title + '">' +
'<div class="dist">' + distLabel + '<span class="u">' + distUnit + '</span></div>' +
'<div><div class="name">' + ev.title + '</div><span class="when">' + (ev.venue || '') + '</span></div>' +
'<div class="go">→</div>' +
'</div>'
);
}).join('');
panelEl.querySelectorAll('.map-row').forEach(function (row) {
row.addEventListener('click', function () {
var id = row.getAttribute('data-id');
var ev = visible.filter(function (e) { return String(e.id) === String(id); })[0];
if (!ev || !map) return;
map.flyTo([ev.lat, ev.lon], 15);
var marker = markers[id];
if (marker) {
if (clusterGroup && clusterGroup.zoomToShowLayer) {
clusterGroup.zoomToShowLayer(marker, function () { marker.openPopup(); });
} else {
marker.openPopup();
}
}
highlightRow(id);
});
});
}
if (map) fitToMarkers(map, markers);
updateUrl();
}
// Moving further than the pool's own coverage (maxRangeKm around the
// server-resolved center) can't be answered from what's already
// loaded — that combination might reach past maxRangeKm even if the
// new point itself is inside it, so the check is deliberately
// conservative: reload whenever full coverage isn't guaranteed,
// rather than risk silently showing an incomplete result.
function setSource(lat, lon) {
var distFromOriginal = distanceKm(originalCenter[0], originalCenter[1], lat, lon);
if (distFromOriginal + state.rangeKm > maxRangeKm) {
var url = new URL(window.location.href);
url.searchParams.set('lat', lat.toFixed(5));
url.searchParams.set('lon', lon.toFixed(5));
url.searchParams.set('range', state.rangeKm);
window.location.href = url.toString();
return;
}
state.source = [lat, lon];
if (sourceMarker) sourceMarker.setLatLng(state.source);
if (map) map.panTo(state.source);
render();
}
if (map) {
map.on('click', function (e) { setSource(e.latlng.lat, e.latlng.lng); });
}
if (sourceMarker) {
sourceMarker.on('dragend', function () {
var ll = sourceMarker.getLatLng();
setSource(ll.lat, ll.lng);
});
}
var rangeSelect = document.getElementById('rangeSelect');
if (rangeSelect) {
rangeSelect.addEventListener('change', function () {
state.rangeKm = parseInt(rangeSelect.value, 10) || state.rangeKm;
render();
});
}
var chipsEl = document.getElementById('catChips');
if (chipsEl) {
chipsEl.addEventListener('click', function (e) {
var btn = e.target.closest('.chip-sm');
if (!btn) return;
chipsEl.querySelectorAll('.chip-sm').forEach(function (c) { c.classList.remove('is-active'); });
btn.classList.add('is-active');
state.cat = btn.getAttribute('data-cat');
render();
});
}
// Nominatim: explicit submit only, never as-you-type — required by
// its usage policy (max ~1 req/s, no autocomplete-style querying).
var locationForm = document.getElementById('locationSearchForm');
if (locationForm) {
locationForm.addEventListener('submit', function (e) {
e.preventDefault();
var input = document.getElementById('locationSearch');
var query = (input.value || '').trim();
if (!query) return;
var statusEl = document.getElementById('locateStatus');
if (statusEl) statusEl.textContent = 'Looking up "' + query + '"…';
fetch('https://nominatim.openstreetmap.org/search?format=json&limit=1&q=' + encodeURIComponent(query))
.then(function (res) { return res.json(); })
.then(function (results) {
if (!results || !results.length) {
if (statusEl) statusEl.textContent = 'Could not find "' + query + '"';
return;
}
setSource(parseFloat(results[0].lat), parseFloat(results[0].lon));
})
.catch(function () {
if (statusEl) statusEl.textContent = 'Location search failed';
});
});
}
function requestLocation() {
var statusEl = document.getElementById('locateStatus');
if (!navigator.geolocation) {
if (statusEl) statusEl.textContent = 'Location not supported by your browser';
return;
}
if (statusEl) statusEl.textContent = 'Finding you…';
navigator.geolocation.getCurrentPosition(function (pos) {
setSource(pos.coords.latitude, pos.coords.longitude);
}, function () {
if (statusEl) statusEl.textContent = "Couldn't get your location";
}, { timeout: 8000 });
}
var locateBtn = document.getElementById('locateBtn');
if (locateBtn) locateBtn.addEventListener('click', requestLocation);
if (navigator.permissions && navigator.permissions.query) {
navigator.permissions.query({ name: 'geolocation' }).then(function (status) {
if (status.state === 'granted') requestLocation();
}).catch(function () {});
}
render();
return { map: map };
}
window.Vindn = window.Vindn || {};
window.Vindn.categoryColor = categoryColor;
window.Vindn.categoryMarkerHex = categoryMarkerHex;
window.Vindn.dotIcon = dotIcon;
window.Vindn.distanceKm = distanceKm;
window.Vindn.renderPinMap = renderPinMap;
window.Vindn.readEmbeddedJson = readEmbeddedJson;
window.Vindn.initMapStage = initMapStage;
window.Vindn.initRadiusMapStage = initRadiusMapStage;
})(window);
+52
View File
@@ -0,0 +1,52 @@
(function () {
var burger = document.querySelector('.nav-burger');
var menu = document.getElementById('mobileMenu');
if (!burger || !menu) return;
function closeMenu() {
menu.hidden = true;
burger.classList.remove('is-open');
burger.setAttribute('aria-expanded', 'false');
}
function openMenu() {
menu.hidden = false;
burger.classList.add('is-open');
burger.setAttribute('aria-expanded', 'true');
}
burger.addEventListener('click', function () {
if (menu.hidden) {
openMenu();
} else {
closeMenu();
}
});
menu.addEventListener('click', function (e) {
if (e.target.tagName === 'A') {
closeMenu();
}
});
document.addEventListener('keydown', function (e) {
if (e.key === 'Escape') {
closeMenu();
}
});
document.addEventListener('click', function (e) {
if (!menu.hidden && !menu.contains(e.target) && !burger.contains(e.target)) {
closeMenu();
}
});
// Desktop CSS hides .mobile-menu entirely, but if the menu was left open
// on mobile and the viewport grows past the breakpoint (resize, rotate,
// devtools), reset state so it doesn't reopen collapsed at mobile width.
window.matchMedia('(min-width: 761px)').addEventListener('change', function (e) {
if (e.matches) {
closeMenu();
}
});
})();
+429
View File
@@ -0,0 +1,429 @@
/* ============================================================
VINDN — Events platform
Design concept: the "board" — every event is treated like an
entry on a departure board (date / time / place rendered in
tabular mono type on a dark strip). It is the one motif that
repeats across hero, listings, search results and detail page,
tying "find an event" to the universal feeling of checking
what's on, right now, near you.
============================================================ */
@import url('https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@500;600;700&family=Inter:wght@400;500;600&family=IBM+Plex+Mono:wght@400;500;600&display=swap');
:root{
/* ---- color ---- */
--paper: #F7F7F4;
--paper-dim: #EFEFE9;
--ink: #14140F;
--ink-soft: #3C3C34;
--muted: #74746A;
--line: #DEDED5;
--line-strong: #C7C7BC;
--accent: #2440FF;
--accent-deep: #1530CC;
--accent-soft: #E7EAFF;
--warn: #C97A00;
--warn-soft: #FFEFD2;
--board-bg: #14140F;
--board-bg-2: #1D1D16;
--board-text: #F4F3EC;
--board-muted: #9A9A8C;
--board-accent: #6E86FF;
--board-warn: #FFC04D;
/* ---- type ---- */
--f-display: 'Space Grotesk', sans-serif;
--f-body: 'Inter', sans-serif;
--f-mono: 'IBM Plex Mono', monospace;
/* ---- shape / space ---- */
--r-card: 10px;
--r-pill: 999px;
--r-flat: 2px;
--gutter: clamp(20px, 5vw, 64px);
--maxw: 1240px;
}
*{ box-sizing: border-box; }
html{ scroll-behavior: smooth; }
body{
margin:0;
background: var(--paper);
color: var(--ink);
font-family: var(--f-body);
font-size: 16px;
line-height: 1.5;
-webkit-font-smoothing: antialiased;
}
img,svg{ max-width:100%; display:block; }
a{ color:inherit; text-decoration:none; }
ul{ margin:0; padding:0; list-style:none; }
button{ font-family:inherit; cursor:pointer; }
.wrap{ max-width: var(--maxw); margin:0 auto; padding:0 var(--gutter); }
/* focus visibility — accessibility floor */
a:focus-visible, button:focus-visible, input:focus-visible, select:focus-visible{
outline: 2px solid var(--accent);
outline-offset: 2px;
border-radius: 3px;
}
@media (prefers-reduced-motion: reduce){
*{ animation-duration: .001ms !important; transition-duration: .001ms !important; }
}
/* ============ type scale ============ */
h1,h2,h3{ font-family: var(--f-display); font-weight:700; letter-spacing:-0.01em; margin:0; }
.eyebrow{
font-family: var(--f-mono);
font-size: 12px;
letter-spacing: 0.08em;
text-transform: uppercase;
color: var(--muted);
}
.mono{ font-family: var(--f-mono); font-variant-numeric: tabular-nums; }
/* ============ top navigation ============ */
.site-header{
position: sticky; top:0; z-index: 50;
background: rgba(247, 247, 244, 0.85);
backdrop-filter: blur(5px);
-webkit-backdrop-filter: blur(5px);
border-bottom: 1px solid var(--line);
}
.site-header .wrap{
display:flex; align-items:center; justify-content:space-between;
height: 76px;
}
.brand{ display:flex; align-items:center; gap:10px; }
.brand img{ height: 76px; width:auto; }
.main-nav{ display:flex; align-items:center; gap: 30px; }
.main-nav a{
font-size: 14px; font-weight:600; color: var(--ink-soft);
padding: 8px 2px; position:relative;
}
.main-nav a.is-active{ color: var(--ink); }
.main-nav a.is-active::after{
content:""; position:absolute; left:0; right:0; bottom:-2px; height:2px; background: var(--accent);
}
.nav-search{
display:flex; align-items:center; gap:8px;
border:1px solid var(--line-strong); border-radius: var(--r-pill);
padding: 8px 14px; font-size:13px; color:var(--muted);
min-width: 220px;
}
.nav-search svg{ width:15px; height:15px; flex:none; }
.nav-search:focus-within{ border-color:var(--accent); }
.nav-search button{ background:none; border:none; padding:0; display:flex; color:inherit; flex:none; }
.nav-search input{ border:none; background:none; outline:none; padding:0; font-family:inherit; font-size:13px; color:var(--ink); flex:1; min-width:0; }
.nav-search input::placeholder{ color:var(--muted); }
.nav-cta{
background: var(--ink); color: var(--paper);
font-size: 13px; font-weight:600;
padding: 11px 18px; border-radius: var(--r-pill);
border:none;
}
.nav-actions{ display:flex; align-items:center; gap:16px; }
.nav-burger{ display:none; background:none; border:none; padding:6px; }
.nav-burger span{ display:block; width:22px; height:2px; background:var(--ink); margin:5px 0; transition:transform .2s ease, opacity .2s ease; }
.nav-burger.is-open span:nth-child(1){ transform:translateY(7px) rotate(45deg); }
.nav-burger.is-open span:nth-child(2){ opacity:0; }
.nav-burger.is-open span:nth-child(3){ transform:translateY(-7px) rotate(-45deg); }
.mobile-menu{ display:none; }
/* ============ buttons ============ */
.btn{
display:inline-flex; align-items:center; gap:8px;
font-family: var(--f-body); font-weight:600; font-size:14px;
padding: 13px 22px; border-radius: var(--r-pill); border:1px solid transparent;
transition: transform .15s ease, background .15s ease;
}
.btn-primary{ background: var(--ink); color:var(--paper); }
.btn-primary:hover{ background: var(--accent-deep); }
.btn-ghost{ background: transparent; color:var(--ink); border-color: var(--line-strong); }
.btn-ghost:hover{ border-color: var(--ink); }
.btn-accent{ background: var(--accent); color:#fff; }
.btn-accent:hover{ background: var(--accent-deep); }
.btn:active{ transform: translateY(1px); }
.btn-sm{ padding:9px 16px; font-size:13px; }
/* ============ chips / tags ============ */
.chip-row{ display:flex; flex-wrap:wrap; gap:10px; }
/* collapsible chip-row: CSS-only checkbox toggle, no JS needed */
.chip-row-collapsible{ max-height:100px; overflow:hidden; }
.chip-toggle-checkbox{ position:absolute; opacity:0; pointer-events:none; }
.chip-toggle-checkbox:checked ~ .chip-row-collapsible{ max-height:none; }
.chip-toggle-btn{
display:inline-flex; margin-top:12px; cursor:pointer;
font-family:var(--f-mono); font-size:12px; font-weight:600; color:var(--accent);
}
.chip-toggle-btn:hover{ color:var(--accent-deep); }
.chip-toggle-btn .show-less{ display:none; }
.chip-toggle-checkbox:checked ~ .chip-toggle-btn .show-more{ display:none; }
.chip-toggle-checkbox:checked ~ .chip-toggle-btn .show-less{ display:inline; }
.chip{
font-family: var(--f-mono); font-size: 12px; letter-spacing:.02em;
padding: 7px 14px; border-radius: var(--r-pill);
border:1px solid var(--line-strong); color: var(--ink-soft);
background: var(--paper); white-space:nowrap;
}
.chip.is-active{ background: var(--ink); color: var(--paper); border-color: var(--ink); }
/* ============ pagination (first/prev/current/next/last) ============ */
.pager{ display:flex; justify-content:center; align-items:center; gap:8px; margin-top:36px; flex-wrap:wrap; }
.pager a, .pager span{
padding:9px 16px; border:1px solid var(--line); border-radius:8px;
font-family:var(--f-mono); font-size:13px; white-space:nowrap;
}
.pager a:hover{ border-color:var(--line-strong); }
.pager span.is-active{ background:var(--ink); color:var(--paper); border-color:var(--ink); font-weight:600; }
.tag{
font-family: var(--f-mono); font-size:11px; letter-spacing:.04em; text-transform:uppercase;
padding: 4px 9px; border-radius: var(--r-flat); background: var(--accent-soft); color: var(--accent-deep);
}
.tag.tag-warn{ background: var(--warn-soft); color: var(--warn); }
.tag.tag-dark{ background: var(--ink); color: var(--paper); }
/* ============ category glyph (replaces stock photography) ============
Small abstract line-glyphs per category drawn with currentColor,
set on a soft tinted tile — keeps the system illustrative and
consistent instead of relying on photography. */
.glyph-tile{
display:flex; align-items:center; justify-content:center;
border-radius: var(--r-card);
aspect-ratio: 4/3;
position:relative; overflow:hidden;
}
.glyph-tile svg{ width:34%; height:34%; }
.glyph-tile::after{
content:""; position:absolute; inset:0;
background-image: radial-gradient(circle at 20% 20%, rgba(255,255,255,.5), transparent 45%);
}
.t-music{ background:#E7EAFF; color:#2440FF; }
.t-tech{ background:#E4F3EC; color:#1F8A56; }
.t-community{ background:#FFEFD2; color:#C97A00; }
.t-sports{ background:#FDE3E0; color:#D14B3A; }
.t-arts{ background:#F1E6FB; color:#7A3FC9; }
.t-food{ background:#FFF1DC; color:#B9740B; }
/* ============ departure board — the signature motif ============ */
.board{
background: var(--board-bg);
border-radius: var(--r-card);
color: var(--board-text);
overflow:hidden;
}
.board-head{
display:flex; align-items:center; justify-content:space-between;
padding: 16px 22px; border-bottom:1px solid #2B2B22;
}
.board-head .eyebrow{ color: var(--board-muted); }
.board-clock{ font-family:var(--f-mono); font-size:13px; color: var(--board-accent); }
.board-row{
display:grid;
grid-template-columns: 96px 1fr 140px 90px;
align-items:center;
gap: 18px;
padding: 16px 22px;
border-bottom: 1px solid #232319;
font-family: var(--f-mono);
}
.board-row:last-child{ border-bottom:none; }
.board-row:hover{ background: var(--board-bg-2); }
.board-date{ color: var(--board-accent); font-size:13px; letter-spacing:.03em; }
.board-date .dow{ display:block; color: var(--board-muted); font-size:11px; margin-top:2px; }
.board-name{ font-family: var(--f-body); font-weight:600; font-size:15px; color: var(--board-text); }
.board-name .where{ display:block; font-family:var(--f-mono); font-weight:400; font-size:12px; color: var(--board-muted); margin-top:3px; letter-spacing:.02em; }
.board-status{ font-size:12px; text-align:right; color: var(--board-muted); }
.board-status.is-soon{ color: var(--board-warn); }
.board-status.is-live{ color: #6CE3A0; }
.board-go{ text-align:right; font-size:12px; color: var(--board-accent); }
/* ============ section scaffolding ============ */
.section{ padding: 64px 0; }
.section-head{
display:flex; align-items:flex-end; justify-content:space-between;
gap: 20px; margin-bottom: 28px; flex-wrap:wrap;
}
.section-head h2{ font-size: clamp(24px,3vw,32px); }
.section-link{ font-size:13px; font-weight:600; color:var(--accent); display:flex; gap:6px; align-items:center; }
/* ============ card grid ============ */
.card-grid{ display:grid; grid-template-columns: repeat(3,1fr); gap: 22px; }
.event-card{
border:1px solid var(--line); border-radius: var(--r-card); background:#fff;
overflow:hidden; display:flex; flex-direction:column;
transition: border-color .15s ease, transform .15s ease;
}
.event-card:hover{ border-color: var(--line-strong); transform: translateY(-2px); }
.event-card .body{ padding: 16px 18px 18px; display:flex; flex-direction:column; gap:8px; flex:1; }
.event-card .meta-row{ display:flex; align-items:center; gap:10px; }
.event-card h3{ font-size:16px; font-family: var(--f-body); font-weight:600; line-height:1.3; }
.event-card .when{ font-family: var(--f-mono); font-size:12px; color: var(--muted); }
.event-card .place{ font-size:13px; color: var(--muted); margin-top:auto; }
.event-card .price{ font-family:var(--f-mono); font-weight:600; font-size:13px; }
/* ============ footer ============ */
.site-footer{ border-top:1px solid var(--line); padding: 48px 0 28px; margin-top:40px; }
.footer-grid{ display:grid; grid-template-columns: 1.4fr 1fr 1fr 1fr; gap: 32px; }
.footer-grid h4{ font-family:var(--f-mono); font-size:12px; text-transform:uppercase; letter-spacing:.06em; color:var(--muted); margin:0 0 14px; }
.footer-grid li{ margin-bottom:10px; font-size:14px; }
.footer-grid li a:hover{ color: var(--accent); }
.footer-bottom{ display:flex; justify-content:space-between; align-items:center; margin-top:40px; padding-top:20px; border-top:1px solid var(--line); font-size:12px; color:var(--muted); font-family:var(--f-mono); }
/* ============ misc utility ============ */
.divider{ height:1px; background:var(--line); border:none; margin: 0; }
.skip-link{ position:absolute; left:-999px; }
.skip-link:focus{ left: 12px; top: 12px; background: var(--ink); color:#fff; padding:8px 14px; border-radius:6px; z-index:100; }
/* ============ responsive ============ */
@media (max-width: 980px){
.card-grid{ grid-template-columns: repeat(2,1fr); }
.footer-grid{ grid-template-columns: 1.4fr 1fr 1fr; }
.board-row{ grid-template-columns: 80px 1fr 76px; }
.board-go{ display:none; }
}
@media (max-width: 760px){
.main-nav, .nav-search{ display:none; }
.nav-burger{ display:block; }
.card-grid{ grid-template-columns: 1fr 1fr; }
.footer-grid{ grid-template-columns: 1fr 1fr; }
.section{ padding: 44px 0; }
.mobile-menu{
display:block;
position:absolute; top:100%; left:0; right:0; z-index:49;
background:var(--paper); border-bottom:1px solid var(--line);
padding:8px 20px 20px;
box-shadow:0 10px 30px rgba(20,20,15,.08);
}
.mobile-menu[hidden]{ display:none; }
.mobile-menu nav{ display:flex; flex-direction:column; }
.mobile-menu nav a{
padding:14px 0; font-size:15px; font-weight:600; color:var(--ink-soft);
border-bottom:1px solid var(--line);
}
.mobile-menu nav a.is-active{ color:var(--ink); }
.mobile-menu .mobile-search{ display:flex; gap:8px; margin-top:14px; }
.mobile-menu .mobile-search input{
flex:1; min-width:0; border:1px solid var(--line-strong); border-radius:var(--r-pill);
padding:10px 14px; font-size:14px; font-family:inherit; color:var(--ink); background:none;
}
.mobile-menu .mobile-search button{
background:var(--ink); color:var(--paper); border:none; border-radius:var(--r-pill);
padding:10px 16px; font-size:13px; font-weight:600;
}
}
@media (max-width: 520px){
.card-grid{ grid-template-columns: 1fr; }
.board-row{ grid-template-columns: 70px 1fr; }
.board-status{ display:none; }
}
/* ============ interactive map stage (home hero + /map) ============ */
.stage{ position:relative; height: calc(100vh - 76px); min-height:520px; }
.stage-compact{ height:560px; min-height:420px; border-radius:var(--r-card); overflow:hidden; box-shadow:0 10px 30px rgba(20,20,15,.08); }
.stage-compact .panel{ width:300px; }
.stage:fullscreen{ height:100vh; border-radius:0; }
.stage:-webkit-full-screen{ height:100vh; border-radius:0; }
#mapCanvas{ position:absolute; inset:0; background: var(--paper-dim); z-index: 10;}
.leaflet-tile-pane, .maplibregl-map{ filter: grayscale(0.85) brightness(1.08) contrast(0.92); }
.leaflet-popup-content-wrapper{ border-radius:10px; box-shadow:0 10px 30px rgba(20,20,15,.18); }
.leaflet-popup-content{ margin:14px 16px; font-family:var(--f-body); }
.leaflet-control-attribution{ font-size:10px; }
.map-overlay-top{
position:absolute; top:5px; left:50px; right:10px; z-index:30;
display:flex; align-items:flex-start; justify-content:space-between; gap:14px; flex-wrap:wrap;
pointer-events:none;
}
.float-card{
background: rgba(247,247,244,0.85);
backdrop-filter: blur(5px);
-webkit-backdrop-filter: blur(5px);
border:1px solid var(--line); border-radius:14px;
box-shadow: 0 8px 24px rgba(20,20,15,.10);
pointer-events:auto;
}
.search-card{ padding:14px 16px; flex:1; max-width:560px; }
.search-card form{ display:flex; gap:10px; margin-bottom:12px; }
.search-card input{ flex:1; padding:11px 14px; border:1px solid var(--line-strong); border-radius:var(--r-pill); font-size:14px; }
.chip-row-tight{ display:flex; gap:8px; flex-wrap:wrap; max-height:64px; overflow-y:auto; }
.chip-sm{ font-family:var(--f-mono); font-size:11px; padding:6px 12px; border-radius:var(--r-pill); border:1px solid var(--line-strong); background:#fff; color:var(--ink-soft); }
.chip-sm.is-active{ background:var(--ink); color:var(--paper); border-color:var(--ink); }
.locate-card{ padding:10px; flex:none; width:220px; }
#locationSearchForm{ display:flex; gap:6px; margin-bottom:8px; }
#locationSearchForm input{ flex:1; min-width:0; padding:9px 12px; border:1px solid var(--line-strong); border-radius:var(--r-pill); font-size:12px; }
.locate-btn{
display:flex; align-items:center; gap:8px; background:var(--ink); color:var(--paper);
border:none; border-radius:var(--r-pill); padding:11px 16px; font-size:13px; font-weight:600; width:100%;
}
.range-label{
display:flex; align-items:center; gap:8px; margin-top:10px;
font-family:var(--f-mono); font-size:11px; color:var(--muted);
}
.range-label select{
flex:1; padding:6px 10px; border:1px solid var(--line-strong); border-radius:6px;
font-family:var(--f-mono); font-size:12px; background:#fff; color:var(--ink);
}
.locate-status{ font-family:var(--f-mono); font-size:11px; color:var(--muted); margin-top:8px; max-width:200px; }
.legend{
position:absolute; left:18px; bottom:18px; z-index:30; padding:10px 14px;
display:flex; gap:14px; flex-wrap:wrap; max-width:30vw; flex-direction: column;
}
.legend span{ display:flex; align-items:center; gap:6px; font-family:var(--f-mono); font-size:11px; color:var(--ink-soft); }
.legend .dot{ width:9px; height:9px; border-radius:50%; }
.panel{
position:absolute; top:200px; right:10px; bottom:18px; width:360px; z-index:25;
display:flex; flex-direction:column; overflow:hidden;
}
/* Overrides .board's solid background just for the map's floating events
panel, to match the other map overlay elements — other .board usages
(event cards, home hero board) keep their solid background. */
.panel.board{
background: rgba(20,20,15,0.85);
backdrop-filter: blur(5px);
-webkit-backdrop-filter: blur(5px);
}
.panel-head{ padding:16px 18px; border-bottom:1px solid var(--line); display:flex; justify-content:space-between; align-items:center; }
.panel-head .count{ font-family:var(--f-mono); font-size:12px; color:var(--muted); }
.panel-list{ overflow-y:auto; flex:1; }
.map-row{ display:grid; grid-template-columns: 58px 1fr 22px; gap:12px; align-items:center; padding:13px 18px; border-bottom:1px solid #232319; cursor:pointer; }
.map-row.no-dist{ grid-template-columns: 1fr 22px; }
.map-row:last-child{ border-bottom:none; }
.map-row:hover, .map-row.is-highlighted{ background:var(--board-bg-2); }
.map-row .dist{ font-family:var(--f-mono); font-size:13px; color:var(--board-accent); }
.map-row .dist .u{ display:block; font-size:10px; color:var(--board-muted); }
.map-row .name{ font-family:var(--f-body); font-weight:600; font-size:13.5px; color:var(--board-text); line-height:1.3; }
.map-row .when{ display:block; font-family:var(--f-mono); font-size:11px; color:var(--board-muted); margin-top:3px; }
.map-row .go{ color:var(--board-accent); text-align:right; }
.pin-pop{ min-width:170px; }
.pin-pop .cat{ font-family:var(--f-mono); font-size:10px; text-transform:uppercase; letter-spacing:.05em; }
.pin-pop h4{ font-size:14px; margin:6px 0 4px; font-family:var(--f-body); font-weight:600; }
.pin-pop p{ font-family:var(--f-mono); font-size:11.5px; color:var(--muted); margin:0 0 8px; }
.pin-pop a{ font-size:12px; font-weight:600; color:var(--accent); }
/* reskin markercluster's default green/orange bubbles to the site's ink tone */
/* !important: MarkerCluster.Default.css loads after this file (in each
page's head_extra block), so an equal-specificity override would lose
the tie without it. */
.marker-cluster-small, .marker-cluster-medium, .marker-cluster-large{ background-color: rgba(20,20,15,.18) !important; }
.marker-cluster-small div, .marker-cluster-medium div, .marker-cluster-large div{ background-color: var(--ink) !important; }
.marker-cluster span{ color:#fff !important; font-family:var(--f-mono); font-weight:600; }
@media (max-width: 900px){
.stage{ height:auto; }
.stage-compact{ height:auto; }
#mapCanvas{ position:static; height:62vh; }
.panel, .stage-compact .panel{ position:static; width:auto; height:auto; max-height:50vh; margin-top:14px; }
.map-overlay-top{ position:absolute; }
.legend{ display:none; }
}