@chrtco/sdk 0.10.30 → 0.10.32

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -28618,466 +28618,149 @@ function worldMinusBboxPolygon(bbox) {
28618
28618
  };
28619
28619
  }
28620
28620
 
28621
- // src/atlas/land-layers.ts
28622
- var LAND_SURFACE_MINZOOM = BAND_PRIORITY_RANGES["5"].minzoom;
28623
- var LAND_SURFACE_LAYERS = (id) => id === "chrtland-base" || id === "chrtland-cover" || id === "chrtland-contour" || id === "chrtland-hillshade" || id.startsWith("chrtland-fade-");
28624
- var isLandGroundOverlay = (id) => LAND_SURFACE_LAYERS(id) && id !== "chrtland-base" || id === "chrtland-road";
28625
- var FADE_PX = 80;
28626
- var FADE_BANDS = 16;
28627
- function fadeAlpha(r) {
28628
- return Math.max(0, Math.min(1, (FADE_PX / 2 - Math.abs(r)) / FADE_PX));
28629
- }
28630
- function landBaseColor(palette) {
28631
- return (LAND_THEMES[palette] ?? LAND_THEMES.DAY).base;
28621
+ // src/atlas/base-layers.ts
28622
+ var NOCOVERAGE_PATTERN = "chrt:hatch-nocoverage";
28623
+ var UNCHARTED_PATTERN = "chrt:hatch-uncharted";
28624
+ function inlandWaterColor(palette) {
28625
+ return (BASE_THEMES[palette] ?? BASE_THEMES.DAY).inlandWater;
28632
28626
  }
28633
- var LAND_THEMES = {
28627
+ var BASE_THEMES = {
28634
28628
  DAY: {
28635
- base: "#e8d9b0",
28636
- cover: { wood: "#cfd8b4", wetland: "#c9d3c4", urban: "#dedad2", sand: "#efe4c2" },
28637
- contour: "#a89878",
28638
- contourIndex: "#8a7a5c",
28639
- road: "#d9c9a4",
28640
- roadMajor: "#d2bd8e",
28641
- label: "#5b4f3a",
28642
- halo: "#f2ead6",
28643
- shadow: "#6b5a3e",
28644
- highlight: "#fffaf0",
28645
- fade: "#e8f2f8",
28646
- // Raised from 0.45 on 2026-08-23. ⚠ Above the ~0.5 that
28647
- // hillshade-style.mjs warns "starts competing with depth areas" — a
28648
- // deliberate, sighted choice made against rendered evidence, not a
28649
- // default. If soundings or DEPARE ever read as fighting the terrain,
28650
- // this number is the first thing to walk back.
28651
- exaggeration: 0.8
28629
+ // ── `earth` is the jurisdiction tan DIMMED (~0.55), not a blue-grey ──
28630
+ // Outside the jurisdiction the map still owes the reader "what is land".
28631
+ // State 3 in the header is "the geography, under a crosshatch", and until
28632
+ // 2026-08-31 that geography was unreadable: earth #2A3439 against sea
28633
+ // #1B2328 measured 1.25:1, so Hispaniola showed charted boxes with tan
28634
+ // land inside and nothing recognisable around them (owner report: "no
28635
+ // land painting down near the DR at some zooms"). Land is warm and sea is
28636
+ // cool because that is what land and sea are; dimming the JURISDICTION
28637
+ // tan rather than inventing a colour says the true thing — this is the
28638
+ // same land, we simply do not carry charts for it. Held against BOTH
28639
+ // neighbours: legible over sea, and never mistakable for the full tan of
28640
+ // a country we do carry. DAY 3.59:1 vs sea, 3.17:1 vs tan.
28641
+ earth: "#807761",
28642
+ water: "#1B2328",
28643
+ river: "#24313A",
28644
+ boundary: "#4A575E",
28645
+ jurisdiction: "#D4C69D",
28646
+ // CHRTland's own tan — one land colour
28647
+ inlandWater: "#B9CFDD",
28648
+ // muted, unmistakably not a charted blue
28649
+ // State furniture. These three were referenced by the layers but missing
28650
+ // from this table (and its type — the long-standing TS2339 trio), so the
28651
+ // lines shipped on MapLibre's DEFAULT black. Now deliberate: a hairline
28652
+ // a shade deeper than the tan, quiet caps, tan halo.
28653
+ stateLine: "#B1A077",
28654
+ stateLabel: "#8E8468",
28655
+ stateHalo: "#D4C69D",
28656
+ // Rough-lake furniture (owner ask 2026-08-28: "the lakes need labels,
28657
+ // and shorelines drawn"): a shoreline a shade deeper than the rough
28658
+ // water, and a name in the water's own voice, haloed by the water.
28659
+ inlandShore: "#8FA9BA",
28660
+ inlandLabel: "#54707F",
28661
+ inlandHalo: "#B9CFDD"
28652
28662
  },
28653
28663
  DUSK: {
28654
- base: "#4a4433",
28655
- cover: { wood: "#414a38", wetland: "#3f4740", urban: "#4e4a42", sand: "#544c39" },
28656
- contour: "#6b6250",
28657
- contourIndex: "#837961",
28658
- road: "#565040",
28659
- roadMajor: "#63594440",
28660
- label: "#c3b898",
28661
- halo: "#2b2820",
28662
- shadow: "#2e2a20",
28663
- highlight: "#8a8068",
28664
- fade: "#16202a",
28665
- // Unchanged. DUSK was not the palette the 0.8 decision was made against.
28666
- exaggeration: 0.45
28664
+ // ── Re-derived 2026-09-01: the basemap sits UNDER the chart ──────────
28665
+ // These were the DAY tones scaled by a constant, never measured against
28666
+ // the DUSK chart — so `jurisdiction` came out at L=0.506 against a chart
28667
+ // whose own land (LANDA #8a7d5e) is L=0.209, i.e. the context layer was
28668
+ // 2.4x brighter than the data layer. Now capped at the chart's own land.
28669
+ // The rest of the block rides the same 0.666 channel factor so every
28670
+ // relationship the palette already chose is preserved, only moved down.
28671
+ // `water` is deliberately NOT scaled: it is the reference the land/sea
28672
+ // floor is measured against.
28673
+ earth: "#544E3E",
28674
+ water: "#161D21",
28675
+ river: "#141B20",
28676
+ boundary: "#293135",
28677
+ jurisdiction: "#867D64",
28678
+ inlandWater: "#62717A",
28679
+ // Label stays DARK here — the halo is still mid-toned, so 3:1 is reachable
28680
+ // downward and a bright label would be a new light source at dusk.
28681
+ stateLine: "#B5A987",
28682
+ stateLabel: "#38342A",
28683
+ stateHalo: "#867D64",
28684
+ inlandShore: "#4D5B67",
28685
+ inlandLabel: "#212629",
28686
+ inlandHalo: "#62717A"
28667
28687
  },
28668
28688
  NIGHT: {
28669
- base: "#2a2620",
28670
- cover: { wood: "#242a20", wetland: "#232823", urban: "#2c2925", sand: "#302b22" },
28671
- contour: "#413b30",
28672
- contourIndex: "#524a3b",
28673
- road: "#332f27",
28674
- roadMajor: "#3d382d",
28675
- label: "#7d745e",
28676
- halo: "#15130f",
28677
- shadow: "#1a1712",
28678
- highlight: "#4a4436",
28679
- fade: "#07090b",
28680
- // ⚠ DELIBERATELY NOT RAISED. See the type's note: a strong hillshade
28681
- // under red night vision is a grey haze over exactly the contrast a
28682
- // skipper is navigating by. Left at the shipped value rather than
28683
- // lowered, because lowering it is a separate decision from this one.
28684
- exaggeration: 0.45
28689
+ // ── Re-derived 2026-09-01 — the owner-reported night defect ──────────
28690
+ // `jurisdiction` was L=0.240 against a chart whose own land (LANDA
28691
+ // #3a3020) is L=0.031: the world basemap was 7.7x BRIGHTER than the chart
28692
+ // it sits under, so at z<11 the uncharted interior read as a pale
28693
+ // continent-sized slab on a night chart. Capped at the chart's own land.
28694
+ //
28695
+ // ⚠ `earth` AND `jurisdiction` CONVERGE HERE, and that is forced, not
28696
+ // sloppy. Holding earth 2:1 above the ocean AND 2:1 below the jurisdiction
28697
+ // tan requires jurisdiction >= L 0.174 — 5.6x the chart's own land. At
28698
+ // night the range between the ocean (L 0.006) and the cap (L 0.031) is too
28699
+ // compressed to carry two land tones. COVERAGE IS STILL SAID, by the
28700
+ // `chrt:hatch-nocoverage` crosshatch, which is a stronger signal than tone
28701
+ // and is the one thing that cannot be lost to palette compression.
28702
+ //
28703
+ // Labels invert to light-on-dark: 3:1 downward is unreachable off a halo
28704
+ // this close to black, and CHRTland's own night town labels already sit at
28705
+ // L=0.177 for exactly this reason.
28706
+ earth: "#333026",
28707
+ water: "#10161A",
28708
+ river: "#090D0F",
28709
+ boundary: "#131719",
28710
+ jurisdiction: "#353128",
28711
+ inlandWater: "#232A30",
28712
+ stateLine: "#484236",
28713
+ stateLabel: "#837963",
28714
+ stateHalo: "#353128",
28715
+ inlandShore: "#1C2227",
28716
+ inlandLabel: "#617484",
28717
+ inlandHalo: "#232A30"
28685
28718
  }
28686
28719
  };
28687
- var FITS = ["<=", ["coalesce", ["get", "min_z"], 0], ["zoom"]];
28688
- var ANCHOR = ["coalesce", ["get", "anchor"], "top"];
28689
- var anchorOffset = (em) => [
28690
- "match",
28691
- ["get", "anchor"],
28692
- "left",
28693
- ["literal", [0.6, 0]],
28694
- "right",
28695
- ["literal", [-0.6, 0]],
28696
- "bottom",
28697
- ["literal", [0, -em]],
28698
- ["literal", [0, em]]
28699
- ];
28700
- var FONT = ["Open Sans Regular"];
28701
- var FONT_ITALIC = ["Open Sans Italic"];
28702
- function landLayers(opts = {}) {
28720
+ function baseLayers(opts = {}) {
28703
28721
  const palette = opts.palette ?? "DAY";
28704
- const T = LAND_THEMES[palette] ?? LAND_THEMES.DAY;
28705
- const src = opts.sourceId ?? LAND_REGION;
28706
- const demSrc = opts.reliefSourceId ?? RELIEF_REGION;
28722
+ const T = BASE_THEMES[palette] ?? BASE_THEMES.DAY;
28723
+ const src = opts.sourceId ?? BASE_REGION;
28707
28724
  const L = (o) => o;
28708
- const layers = [
28709
- // 1. The land itself — LNDARE ∩ coverage, so it can never outrun the chart.
28725
+ return [
28726
+ // 1. Geography — the world as it is, in the quiet dark voice. The OCEAN
28727
+ // is a background, not a polygon: the archive ships land and lakes,
28728
+ // and "everything else is sea" is exactly what a background expresses
28729
+ // — geometry-free, gap-free at every antimeridian and pole. Without
28730
+ // it the sea outside coverage was bare S-52 NODTA, a light grey that
28731
+ // read as "charted ground" with a hatch on it.
28710
28732
  L({
28711
- id: "chrtland-base",
28733
+ id: "chrtbase-ocean",
28734
+ type: "background",
28735
+ paint: { "background-color": T.water }
28736
+ }),
28737
+ L({
28738
+ id: "chrtbase-earth",
28712
28739
  type: "fill",
28713
28740
  source: src,
28714
- "source-layer": "land_base",
28715
- paint: { "fill-color": T.base }
28716
- })
28717
- ];
28718
- layers.push(L({
28719
- id: "chrtland-cover",
28720
- type: "fill",
28721
- source: src,
28722
- "source-layer": "land_cover",
28723
- filter: ["<=", ["coalesce", ["get", "min_z"], 0], ["zoom"]],
28724
- paint: {
28725
- "fill-color": [
28726
- "match",
28727
- ["get", "class"],
28728
- "wood",
28729
- T.cover.wood,
28730
- "wetland",
28731
- T.cover.wetland,
28732
- "urban",
28733
- T.cover.urban,
28734
- "sand",
28735
- T.cover.sand,
28736
- T.base
28737
- ],
28738
- "fill-opacity": 0.85
28739
- }
28740
- }));
28741
- if (opts.relief) {
28742
- layers.push(L({
28743
- // No minzoom here: LAND_SURFACE_LAYERS floors it with the rest of the
28744
- // surface, so the two can never drift apart again.
28745
- id: "chrtland-hillshade",
28746
- type: "hillshade",
28747
- source: demSrc,
28741
+ "source-layer": "base_earth",
28742
+ paint: { "fill-color": T.earth }
28743
+ }),
28744
+ L({
28745
+ id: "chrtbase-water",
28746
+ type: "fill",
28747
+ source: src,
28748
+ "source-layer": "base_water",
28749
+ paint: { "fill-color": T.water }
28750
+ }),
28751
+ L({
28752
+ id: "chrtbase-river",
28753
+ type: "line",
28754
+ source: src,
28755
+ "source-layer": "base_river",
28756
+ minzoom: 9,
28748
28757
  paint: {
28749
- "hillshade-exaggeration": T.exaggeration,
28750
- "hillshade-shadow-color": T.shadow,
28751
- "hillshade-highlight-color": T.highlight,
28752
- "hillshade-accent-color": T.shadow
28758
+ "line-color": T.river,
28759
+ "line-width": ["interpolate", ["linear"], ["zoom"], 9, 0.6, 14, 1.6]
28753
28760
  }
28754
- }));
28755
- }
28756
- const isIndexContour = [
28757
- "any",
28758
- ["<=", ["coalesce", ["get", "index_z"], 99], ["zoom"]],
28759
- ["==", ["get", "is_index"], 1]
28760
- ];
28761
- layers.push(L({
28762
- id: "chrtland-contour",
28763
- type: "line",
28764
- source: src,
28765
- "source-layer": "land_contour",
28766
- filter: ["<=", ["coalesce", ["get", "min_z"], 0], ["zoom"]],
28767
- paint: {
28768
- "line-color": ["case", isIndexContour, T.contourIndex, T.contour],
28769
- "line-width": ["case", isIndexContour, 0.8, 0.4],
28770
- "line-opacity": 0.7
28771
- }
28772
- }));
28773
- const d = FADE_PX / 2 / FADE_BANDS;
28774
- for (let k = 0; k < FADE_BANDS; k++) {
28775
- const width = k === 0 ? { "line-width": 2 * d } : { "line-width": d, "line-gap-width": 2 * k * d };
28776
- layers.push(L({
28777
- id: `chrtland-fade-${k}`,
28778
- type: "line",
28779
- source: src,
28780
- "source-layer": "land_edge",
28781
- layout: { "line-join": "bevel" },
28782
- paint: { "line-color": T.fade, "line-blur": 0, "line-opacity": fadeAlpha(k * d + d / 2), ...width }
28783
- }));
28784
- }
28785
- layers.push(L({
28786
- id: "chrtland-road",
28787
- type: "line",
28788
- source: src,
28789
- "source-layer": "land_road",
28790
- minzoom: 10,
28791
- paint: {
28792
- "line-color": ["match", ["get", "kind"], "highway", T.roadMajor, T.road],
28793
- "line-width": [
28794
- "interpolate",
28795
- ["linear"],
28796
- ["zoom"],
28797
- 10,
28798
- ["match", ["get", "kind"], "highway", 0.8, 0.4],
28799
- 14,
28800
- ["match", ["get", "kind"], "highway", 2.4, 1.2]
28801
- ],
28802
- "line-opacity": 0.8
28803
- }
28804
- }));
28805
- layers.push(L({
28806
- id: "chrtland-city-major",
28807
- type: "symbol",
28808
- source: src,
28809
- "source-layer": "land_town",
28810
- minzoom: 6.5,
28811
- filter: [">=", ["coalesce", ["get", "prank"], 0], 10],
28812
- layout: {
28813
- "text-field": [
28814
- "case",
28815
- ["==", ["get", "upcase"], true],
28816
- ["upcase", ["get", "name"]],
28817
- ["get", "name"]
28818
- ],
28819
- "text-font": FONT,
28820
- "text-size": [
28821
- "interpolate",
28822
- ["linear"],
28823
- ["zoom"],
28824
- 6.5,
28825
- 11.5,
28826
- 9,
28827
- ["+", 10, ["*", 0.4, ["coalesce", ["get", "prank"], 10]]],
28828
- 13,
28829
- ["+", 13, ["*", 0.5, ["coalesce", ["get", "prank"], 10]]]
28830
- ],
28831
- "text-letter-spacing": ["case", ["==", ["get", "upcase"], true], 0.14, 0.02],
28832
- "text-offset": anchorOffset(0.75),
28833
- "text-anchor": ANCHOR,
28834
- "text-padding": 10,
28835
- "text-allow-overlap": false,
28836
- "symbol-sort-key": ["*", -1, ["coalesce", ["get", "pop"], 0]]
28837
- },
28838
- paint: { "text-color": T.label, "text-halo-color": T.halo, "text-halo-width": 1.6 }
28839
- }));
28840
- layers.push(L({
28841
- id: "chrtland-town-port",
28842
- type: "symbol",
28843
- source: src,
28844
- "source-layer": "land_town",
28845
- minzoom: 8.5,
28846
- filter: [
28847
- "all",
28848
- ["==", ["get", "enc_dup"], true],
28849
- ["<", ["coalesce", ["get", "prank"], 0], 10]
28850
- ],
28851
- layout: {
28852
- "text-field": ["get", "name"],
28853
- "text-font": FONT,
28854
- "text-size": ["interpolate", ["linear"], ["zoom"], 8.5, 11, 12, 13.5],
28855
- "text-offset": anchorOffset(0.75),
28856
- "text-anchor": ANCHOR,
28857
- "text-padding": 8,
28858
- "text-allow-overlap": false,
28859
- // Within the tier, the bigger place wins the space: without this the
28860
- // tie broke on data order and "West Gloucester" beat Gloucester.
28861
- "symbol-sort-key": ["*", -1, ["coalesce", ["get", "pop"], 0]]
28862
- },
28863
- paint: { "text-color": T.label, "text-halo-color": T.halo, "text-halo-width": 1.4 }
28864
- }));
28865
- layers.push(L({
28866
- id: "chrtland-town",
28867
- type: "symbol",
28868
- source: src,
28869
- "source-layer": "land_town",
28870
- minzoom: 9,
28871
- filter: [
28872
- "all",
28873
- FITS,
28874
- ["<", ["coalesce", ["get", "prank"], 0], 10],
28875
- ["!=", ["get", "enc_dup"], true]
28876
- ],
28877
- layout: {
28878
- "text-field": [
28879
- "case",
28880
- ["==", ["get", "upcase"], true],
28881
- ["upcase", ["get", "name"]],
28882
- ["get", "name"]
28883
- ],
28884
- "text-font": FONT,
28885
- "text-size": ["interpolate", ["linear"], ["zoom"], 9, 10, 14, 13.5],
28886
- "text-letter-spacing": ["case", ["==", ["get", "upcase"], true], 0.14, 0],
28887
- "text-offset": anchorOffset(0.75),
28888
- "text-anchor": ANCHOR,
28889
- "text-padding": 6,
28890
- "text-allow-overlap": false,
28891
- "symbol-sort-key": 2
28892
- },
28893
- paint: { "text-color": T.label, "text-halo-color": T.halo, "text-halo-width": 1.2 }
28894
- }));
28895
- layers.push(L({
28896
- id: "chrtland-summit",
28897
- type: "symbol",
28898
- source: src,
28899
- "source-layer": "land_summit",
28900
- minzoom: 11.5,
28901
- filter: ["all", FITS, [">=", ["get", "peak_m"], 40]],
28902
- layout: {
28903
- "text-field": ["concat", ["get", "name"], "\n", ["to-string", ["get", "peak_m"]], " m"],
28904
- "text-font": FONT,
28905
- "text-size": ["interpolate", ["linear"], ["zoom"], 11.5, 8.5, 14, 11],
28906
- "text-offset": anchorOffset(0.8),
28907
- "text-anchor": ANCHOR,
28908
- "text-padding": 5,
28909
- "text-allow-overlap": false,
28910
- "symbol-sort-key": 3
28911
- },
28912
- paint: { "text-color": T.label, "text-halo-color": T.halo, "text-halo-width": 1.2 }
28913
- }));
28914
- layers.push(L({
28915
- id: "chrtland-poi",
28916
- type: "symbol",
28917
- source: src,
28918
- "source-layer": "land_poi",
28919
- minzoom: 13,
28920
- filter: FITS,
28921
- layout: {
28922
- "text-field": ["get", "name"],
28923
- "text-font": FONT_ITALIC,
28924
- "text-size": ["interpolate", ["linear"], ["zoom"], 13, 8, 14, 9.5],
28925
- "text-offset": anchorOffset(0.7),
28926
- "text-anchor": ANCHOR,
28927
- "text-padding": 5,
28928
- "text-allow-overlap": false,
28929
- "symbol-sort-key": 5
28930
- },
28931
- paint: { "text-color": T.label, "text-halo-color": T.halo, "text-halo-width": 1.2 }
28932
- }));
28933
- for (const layer of layers) {
28934
- const l = layer;
28935
- if (!LAND_SURFACE_LAYERS(l.id)) continue;
28936
- l.minzoom = Math.max(l.minzoom ?? 0, LAND_SURFACE_MINZOOM);
28937
- }
28938
- return layers;
28939
- }
28940
-
28941
- // src/atlas/base-layers.ts
28942
- var NOCOVERAGE_PATTERN = "chrt:hatch-nocoverage";
28943
- var UNCHARTED_PATTERN = "chrt:hatch-uncharted";
28944
- var BASE_THEMES = {
28945
- DAY: {
28946
- // ── `earth` is the jurisdiction tan DIMMED (~0.55), not a blue-grey ──
28947
- // Outside the jurisdiction the map still owes the reader "what is land".
28948
- // State 3 in the header is "the geography, under a crosshatch", and until
28949
- // 2026-08-31 that geography was unreadable: earth #2A3439 against sea
28950
- // #1B2328 measured 1.25:1, so Hispaniola showed charted boxes with tan
28951
- // land inside and nothing recognisable around them (owner report: "no
28952
- // land painting down near the DR at some zooms"). Land is warm and sea is
28953
- // cool because that is what land and sea are; dimming the JURISDICTION
28954
- // tan rather than inventing a colour says the true thing — this is the
28955
- // same land, we simply do not carry charts for it. Held against BOTH
28956
- // neighbours: legible over sea, and never mistakable for the full tan of
28957
- // a country we do carry. DAY 3.59:1 vs sea, 3.17:1 vs tan.
28958
- earth: "#807761",
28959
- water: "#1B2328",
28960
- river: "#24313A",
28961
- boundary: "#4A575E",
28962
- jurisdiction: "#E8D9B0",
28963
- // CHRTland's own tan — one land colour
28964
- inlandWater: "#B9CFDD",
28965
- // muted, unmistakably not a charted blue
28966
- // State furniture. These three were referenced by the layers but missing
28967
- // from this table (and its type — the long-standing TS2339 trio), so the
28968
- // lines shipped on MapLibre's DEFAULT black. Now deliberate: a hairline
28969
- // a shade deeper than the tan, quiet caps, tan halo.
28970
- stateLine: "#C4B389",
28971
- stateLabel: "#8E8468",
28972
- stateHalo: "#E8D9B0",
28973
- // Rough-lake furniture (owner ask 2026-08-28: "the lakes need labels,
28974
- // and shorelines drawn"): a shoreline a shade deeper than the rough
28975
- // water, and a name in the water's own voice, haloed by the water.
28976
- inlandShore: "#8FA9BA",
28977
- inlandLabel: "#54707F",
28978
- inlandHalo: "#B9CFDD"
28979
- },
28980
- DUSK: {
28981
- // ── Re-derived 2026-09-01: the basemap sits UNDER the chart ──────────
28982
- // These were the DAY tones scaled by a constant, never measured against
28983
- // the DUSK chart — so `jurisdiction` came out at L=0.506 against a chart
28984
- // whose own land (LANDA #8a7d5e) is L=0.209, i.e. the context layer was
28985
- // 2.4x brighter than the data layer. Now capped at the chart's own land.
28986
- // The rest of the block rides the same 0.666 channel factor so every
28987
- // relationship the palette already chose is preserved, only moved down.
28988
- // `water` is deliberately NOT scaled: it is the reference the land/sea
28989
- // floor is measured against.
28990
- earth: "#544E3E",
28991
- water: "#161D21",
28992
- river: "#141B20",
28993
- boundary: "#293135",
28994
- jurisdiction: "#867D64",
28995
- inlandWater: "#62717A",
28996
- // Label stays DARK here — the halo is still mid-toned, so 3:1 is reachable
28997
- // downward and a bright label would be a new light source at dusk.
28998
- stateLine: "#B5A987",
28999
- stateLabel: "#38342A",
29000
- stateHalo: "#867D64",
29001
- inlandShore: "#4D5B67",
29002
- inlandLabel: "#212629",
29003
- inlandHalo: "#62717A"
29004
- },
29005
- NIGHT: {
29006
- // ── Re-derived 2026-09-01 — the owner-reported night defect ──────────
29007
- // `jurisdiction` was L=0.240 against a chart whose own land (LANDA
29008
- // #3a3020) is L=0.031: the world basemap was 7.7x BRIGHTER than the chart
29009
- // it sits under, so at z<11 the uncharted interior read as a pale
29010
- // continent-sized slab on a night chart. Capped at the chart's own land.
29011
- //
29012
- // ⚠ `earth` AND `jurisdiction` CONVERGE HERE, and that is forced, not
29013
- // sloppy. Holding earth 2:1 above the ocean AND 2:1 below the jurisdiction
29014
- // tan requires jurisdiction >= L 0.174 — 5.6x the chart's own land. At
29015
- // night the range between the ocean (L 0.006) and the cap (L 0.031) is too
29016
- // compressed to carry two land tones. COVERAGE IS STILL SAID, by the
29017
- // `chrt:hatch-nocoverage` crosshatch, which is a stronger signal than tone
29018
- // and is the one thing that cannot be lost to palette compression.
29019
- //
29020
- // Labels invert to light-on-dark: 3:1 downward is unreachable off a halo
29021
- // this close to black, and CHRTland's own night town labels already sit at
29022
- // L=0.177 for exactly this reason.
29023
- earth: "#333026",
29024
- water: "#10161A",
29025
- river: "#090D0F",
29026
- boundary: "#131719",
29027
- jurisdiction: "#353128",
29028
- inlandWater: "#232A30",
29029
- stateLine: "#484236",
29030
- stateLabel: "#837963",
29031
- stateHalo: "#353128",
29032
- inlandShore: "#1C2227",
29033
- inlandLabel: "#617484",
29034
- inlandHalo: "#232A30"
29035
- }
29036
- };
29037
- function baseLayers(opts = {}) {
29038
- const palette = opts.palette ?? "DAY";
29039
- const T = BASE_THEMES[palette] ?? BASE_THEMES.DAY;
29040
- const src = opts.sourceId ?? BASE_REGION;
29041
- const L = (o) => o;
29042
- return [
29043
- // 1. Geography — the world as it is, in the quiet dark voice. The OCEAN
29044
- // is a background, not a polygon: the archive ships land and lakes,
29045
- // and "everything else is sea" is exactly what a background expresses
29046
- // — geometry-free, gap-free at every antimeridian and pole. Without
29047
- // it the sea outside coverage was bare S-52 NODTA, a light grey that
29048
- // read as "charted ground" with a hatch on it.
29049
- L({
29050
- id: "chrtbase-ocean",
29051
- type: "background",
29052
- paint: { "background-color": T.water }
29053
- }),
29054
- L({
29055
- id: "chrtbase-earth",
29056
- type: "fill",
29057
- source: src,
29058
- "source-layer": "base_earth",
29059
- paint: { "fill-color": T.earth }
29060
- }),
29061
- L({
29062
- id: "chrtbase-water",
29063
- type: "fill",
29064
- source: src,
29065
- "source-layer": "base_water",
29066
- paint: { "fill-color": T.water }
29067
- }),
29068
- L({
29069
- id: "chrtbase-river",
29070
- type: "line",
29071
- source: src,
29072
- "source-layer": "base_river",
29073
- minzoom: 9,
29074
- paint: {
29075
- "line-color": T.river,
29076
- "line-width": ["interpolate", ["linear"], ["zoom"], 9, 0.6, 14, 1.6]
29077
- }
29078
- }),
29079
- L({
29080
- id: "chrtbase-boundary",
28761
+ }),
28762
+ L({
28763
+ id: "chrtbase-boundary",
29081
28764
  type: "line",
29082
28765
  source: src,
29083
28766
  "source-layer": "base_boundary",
@@ -29344,32 +29027,360 @@ function baseInlandWaterLayers(opts = {}) {
29344
29027
  }
29345
29028
  }),
29346
29029
  L({
29347
- id: "chrtbase-inland-label",
29348
- type: "symbol",
29030
+ id: "chrtbase-inland-label",
29031
+ type: "symbol",
29032
+ source: src,
29033
+ "source-layer": "inland_water_label",
29034
+ minzoom: 5,
29035
+ layout: {
29036
+ "text-field": ["get", "name"],
29037
+ "text-font": ["Open Sans Regular"],
29038
+ "text-size": ["interpolate", ["linear"], ["zoom"], 5, 9.5, 10, 12],
29039
+ "text-max-width": 7,
29040
+ "text-allow-overlap": false,
29041
+ // Zoom-scaled spacing for the anchor GRID: at z8.9 the 0.08-deg
29042
+ // anchors sit ~54 px apart and a flat padding let six copies of
29043
+ // "Lake Ontario" crowd one screen (owner-reported 2026-08-30).
29044
+ // Wide collision boxes at low zoom thin the grid to the repeat-
29045
+ // by-convention cadence; by z12 the anchors are ~850 px apart and
29046
+ // the padding relaxes to normal.
29047
+ "text-padding": ["interpolate", ["linear"], ["zoom"], 5, 80, 9, 64, 12, 16]
29048
+ },
29049
+ paint: {
29050
+ "text-color": T.inlandLabel,
29051
+ "text-halo-color": T.inlandHalo,
29052
+ "text-halo-width": 1.1
29053
+ }
29054
+ })
29055
+ ];
29056
+ }
29057
+
29058
+ // src/atlas/land-layers.ts
29059
+ var LAND_SURFACE_MINZOOM = BAND_PRIORITY_RANGES["5"].minzoom;
29060
+ var LAND_SURFACE_LAYERS = (id) => id === "chrtland-base" || id === "chrtland-cover" || id === "chrtland-contour" || id === "chrtland-hillshade" || id === "chrtland-water" || id.startsWith("chrtland-fade-");
29061
+ var isLandOverlay = (id) => id.startsWith("chrtland-") && id !== "chrtland-base";
29062
+ var FADE_PX = 80;
29063
+ var FADE_BANDS = 16;
29064
+ function fadeAlpha(r) {
29065
+ return Math.max(0, Math.min(1, (FADE_PX / 2 - Math.abs(r)) / FADE_PX));
29066
+ }
29067
+ function landBaseColor(palette) {
29068
+ return (LAND_THEMES[palette] ?? LAND_THEMES.DAY).base;
29069
+ }
29070
+ var LAND_THEMES = {
29071
+ DAY: {
29072
+ base: "#d4c69d",
29073
+ cover: { wood: "#bcc5a1", wetland: "#b6c0b1", urban: "#cac6bf", sand: "#dbd0af" },
29074
+ contour: "#958666",
29075
+ contourIndex: "#78694b",
29076
+ road: "#c5b691",
29077
+ roadMajor: "#beaa7c",
29078
+ label: "#5b4f3a",
29079
+ halo: "#ded6c3",
29080
+ shadow: "#6b5a3e",
29081
+ highlight: "#fffaf0",
29082
+ fade: "#e8f2f8",
29083
+ // Raised from 0.45 on 2026-08-23. ⚠ Above the ~0.5 that
29084
+ // hillshade-style.mjs warns "starts competing with depth areas" — a
29085
+ // deliberate, sighted choice made against rendered evidence, not a
29086
+ // default. If soundings or DEPARE ever read as fighting the terrain,
29087
+ // this number is the first thing to walk back.
29088
+ exaggeration: 0.8
29089
+ },
29090
+ DUSK: {
29091
+ base: "#4a4433",
29092
+ cover: { wood: "#414a38", wetland: "#3f4740", urban: "#4e4a42", sand: "#544c39" },
29093
+ contour: "#6b6250",
29094
+ contourIndex: "#837961",
29095
+ road: "#565040",
29096
+ roadMajor: "#63594440",
29097
+ label: "#c3b898",
29098
+ halo: "#2b2820",
29099
+ shadow: "#2e2a20",
29100
+ highlight: "#8a8068",
29101
+ fade: "#16202a",
29102
+ // Unchanged. DUSK was not the palette the 0.8 decision was made against.
29103
+ exaggeration: 0.45
29104
+ },
29105
+ NIGHT: {
29106
+ base: "#2a2620",
29107
+ cover: { wood: "#242a20", wetland: "#232823", urban: "#2c2925", sand: "#302b22" },
29108
+ contour: "#413b30",
29109
+ contourIndex: "#524a3b",
29110
+ road: "#332f27",
29111
+ roadMajor: "#3d382d",
29112
+ label: "#7d745e",
29113
+ halo: "#15130f",
29114
+ shadow: "#1a1712",
29115
+ highlight: "#4a4436",
29116
+ fade: "#07090b",
29117
+ // ⚠ DELIBERATELY NOT RAISED. See the type's note: a strong hillshade
29118
+ // under red night vision is a grey haze over exactly the contrast a
29119
+ // skipper is navigating by. Left at the shipped value rather than
29120
+ // lowered, because lowering it is a separate decision from this one.
29121
+ exaggeration: 0.45
29122
+ }
29123
+ };
29124
+ var FITS = ["<=", ["coalesce", ["get", "min_z"], 0], ["zoom"]];
29125
+ var ANCHOR = ["coalesce", ["get", "anchor"], "top"];
29126
+ var anchorOffset = (em) => [
29127
+ "match",
29128
+ ["get", "anchor"],
29129
+ "left",
29130
+ ["literal", [0.6, 0]],
29131
+ "right",
29132
+ ["literal", [-0.6, 0]],
29133
+ "bottom",
29134
+ ["literal", [0, -em]],
29135
+ ["literal", [0, em]]
29136
+ ];
29137
+ var FONT = ["Open Sans Regular"];
29138
+ var FONT_ITALIC = ["Open Sans Italic"];
29139
+ function landLayers(opts = {}) {
29140
+ const palette = opts.palette ?? "DAY";
29141
+ const T = LAND_THEMES[palette] ?? LAND_THEMES.DAY;
29142
+ const src = opts.sourceId ?? LAND_REGION;
29143
+ const demSrc = opts.reliefSourceId ?? RELIEF_REGION;
29144
+ const L = (o) => o;
29145
+ const layers = [
29146
+ // 1. The land itself — LNDARE ∩ coverage, so it can never outrun the chart.
29147
+ L({
29148
+ id: "chrtland-base",
29149
+ type: "fill",
29349
29150
  source: src,
29350
- "source-layer": "inland_water_label",
29351
- minzoom: 5,
29352
- layout: {
29353
- "text-field": ["get", "name"],
29354
- "text-font": ["Open Sans Regular"],
29355
- "text-size": ["interpolate", ["linear"], ["zoom"], 5, 9.5, 10, 12],
29356
- "text-max-width": 7,
29357
- "text-allow-overlap": false,
29358
- // Zoom-scaled spacing for the anchor GRID: at z8.9 the 0.08-deg
29359
- // anchors sit ~54 px apart and a flat padding let six copies of
29360
- // "Lake Ontario" crowd one screen (owner-reported 2026-08-30).
29361
- // Wide collision boxes at low zoom thin the grid to the repeat-
29362
- // by-convention cadence; by z12 the anchors are ~850 px apart and
29363
- // the padding relaxes to normal.
29364
- "text-padding": ["interpolate", ["linear"], ["zoom"], 5, 80, 9, 64, 12, 16]
29365
- },
29151
+ "source-layer": "land_base",
29152
+ paint: { "fill-color": T.base }
29153
+ })
29154
+ ];
29155
+ layers.push(L({
29156
+ id: "chrtland-cover",
29157
+ type: "fill",
29158
+ source: src,
29159
+ "source-layer": "land_cover",
29160
+ filter: ["<=", ["coalesce", ["get", "min_z"], 0], ["zoom"]],
29161
+ paint: {
29162
+ "fill-color": [
29163
+ "match",
29164
+ ["get", "class"],
29165
+ "wood",
29166
+ T.cover.wood,
29167
+ "wetland",
29168
+ T.cover.wetland,
29169
+ "urban",
29170
+ T.cover.urban,
29171
+ "sand",
29172
+ T.cover.sand,
29173
+ T.base
29174
+ ],
29175
+ "fill-opacity": 0.85
29176
+ }
29177
+ }));
29178
+ if (opts.relief) {
29179
+ layers.push(L({
29180
+ // No minzoom here: LAND_SURFACE_LAYERS floors it with the rest of the
29181
+ // surface, so the two can never drift apart again.
29182
+ id: "chrtland-hillshade",
29183
+ type: "hillshade",
29184
+ source: demSrc,
29366
29185
  paint: {
29367
- "text-color": T.inlandLabel,
29368
- "text-halo-color": T.inlandHalo,
29369
- "text-halo-width": 1.1
29186
+ "hillshade-exaggeration": T.exaggeration,
29187
+ "hillshade-shadow-color": T.shadow,
29188
+ "hillshade-highlight-color": T.highlight,
29189
+ "hillshade-accent-color": T.shadow
29370
29190
  }
29371
- })
29191
+ }));
29192
+ }
29193
+ const isIndexContour = [
29194
+ "any",
29195
+ ["<=", ["coalesce", ["get", "index_z"], 99], ["zoom"]],
29196
+ ["==", ["get", "is_index"], 1]
29372
29197
  ];
29198
+ layers.push(L({
29199
+ id: "chrtland-contour",
29200
+ type: "line",
29201
+ source: src,
29202
+ "source-layer": "land_contour",
29203
+ filter: ["<=", ["coalesce", ["get", "min_z"], 0], ["zoom"]],
29204
+ paint: {
29205
+ "line-color": ["case", isIndexContour, T.contourIndex, T.contour],
29206
+ "line-width": ["case", isIndexContour, 0.8, 0.4],
29207
+ "line-opacity": 0.7
29208
+ }
29209
+ }));
29210
+ layers.push(L({
29211
+ id: "chrtland-water",
29212
+ type: "fill",
29213
+ source: src,
29214
+ "source-layer": "land_water",
29215
+ filter: ["<=", ["coalesce", ["get", "min_z"], 0], ["zoom"]],
29216
+ paint: { "fill-color": inlandWaterColor(palette) }
29217
+ }));
29218
+ const d = FADE_PX / 2 / FADE_BANDS;
29219
+ for (let k = 0; k < FADE_BANDS; k++) {
29220
+ const width = k === 0 ? { "line-width": 2 * d } : { "line-width": d, "line-gap-width": 2 * k * d };
29221
+ layers.push(L({
29222
+ id: `chrtland-fade-${k}`,
29223
+ type: "line",
29224
+ source: src,
29225
+ "source-layer": "land_edge",
29226
+ layout: { "line-join": "bevel" },
29227
+ paint: { "line-color": T.fade, "line-blur": 0, "line-opacity": fadeAlpha(k * d + d / 2), ...width }
29228
+ }));
29229
+ }
29230
+ layers.push(L({
29231
+ id: "chrtland-road",
29232
+ type: "line",
29233
+ source: src,
29234
+ "source-layer": "land_road",
29235
+ minzoom: 10,
29236
+ paint: {
29237
+ "line-color": ["match", ["get", "kind"], "highway", T.roadMajor, T.road],
29238
+ "line-width": [
29239
+ "interpolate",
29240
+ ["linear"],
29241
+ ["zoom"],
29242
+ 10,
29243
+ ["match", ["get", "kind"], "highway", 0.8, 0.4],
29244
+ 14,
29245
+ ["match", ["get", "kind"], "highway", 2.4, 1.2]
29246
+ ],
29247
+ "line-opacity": 0.8
29248
+ }
29249
+ }));
29250
+ layers.push(L({
29251
+ id: "chrtland-city-major",
29252
+ type: "symbol",
29253
+ source: src,
29254
+ "source-layer": "land_town",
29255
+ minzoom: 6.5,
29256
+ filter: [">=", ["coalesce", ["get", "prank"], 0], 10],
29257
+ layout: {
29258
+ "text-field": [
29259
+ "case",
29260
+ ["==", ["get", "upcase"], true],
29261
+ ["upcase", ["get", "name"]],
29262
+ ["get", "name"]
29263
+ ],
29264
+ "text-font": FONT,
29265
+ "text-size": [
29266
+ "interpolate",
29267
+ ["linear"],
29268
+ ["zoom"],
29269
+ 6.5,
29270
+ 11.5,
29271
+ 9,
29272
+ ["+", 10, ["*", 0.4, ["coalesce", ["get", "prank"], 10]]],
29273
+ 13,
29274
+ ["+", 13, ["*", 0.5, ["coalesce", ["get", "prank"], 10]]]
29275
+ ],
29276
+ "text-letter-spacing": ["case", ["==", ["get", "upcase"], true], 0.14, 0.02],
29277
+ "text-offset": anchorOffset(0.75),
29278
+ "text-anchor": ANCHOR,
29279
+ "text-padding": 10,
29280
+ "text-allow-overlap": false,
29281
+ "symbol-sort-key": ["*", -1, ["coalesce", ["get", "pop"], 0]]
29282
+ },
29283
+ paint: { "text-color": T.label, "text-halo-color": T.halo, "text-halo-width": 1.6 }
29284
+ }));
29285
+ layers.push(L({
29286
+ id: "chrtland-town-port",
29287
+ type: "symbol",
29288
+ source: src,
29289
+ "source-layer": "land_town",
29290
+ minzoom: 8.5,
29291
+ filter: [
29292
+ "all",
29293
+ ["==", ["get", "enc_dup"], true],
29294
+ ["<", ["coalesce", ["get", "prank"], 0], 10]
29295
+ ],
29296
+ layout: {
29297
+ "text-field": ["get", "name"],
29298
+ "text-font": FONT,
29299
+ "text-size": ["interpolate", ["linear"], ["zoom"], 8.5, 11, 12, 13.5],
29300
+ "text-offset": anchorOffset(0.75),
29301
+ "text-anchor": ANCHOR,
29302
+ "text-padding": 8,
29303
+ "text-allow-overlap": false,
29304
+ // Within the tier, the bigger place wins the space: without this the
29305
+ // tie broke on data order and "West Gloucester" beat Gloucester.
29306
+ "symbol-sort-key": ["*", -1, ["coalesce", ["get", "pop"], 0]]
29307
+ },
29308
+ paint: { "text-color": T.label, "text-halo-color": T.halo, "text-halo-width": 1.4 }
29309
+ }));
29310
+ layers.push(L({
29311
+ id: "chrtland-town",
29312
+ type: "symbol",
29313
+ source: src,
29314
+ "source-layer": "land_town",
29315
+ minzoom: 9,
29316
+ filter: [
29317
+ "all",
29318
+ FITS,
29319
+ ["<", ["coalesce", ["get", "prank"], 0], 10],
29320
+ ["!=", ["get", "enc_dup"], true]
29321
+ ],
29322
+ layout: {
29323
+ "text-field": [
29324
+ "case",
29325
+ ["==", ["get", "upcase"], true],
29326
+ ["upcase", ["get", "name"]],
29327
+ ["get", "name"]
29328
+ ],
29329
+ "text-font": FONT,
29330
+ "text-size": ["interpolate", ["linear"], ["zoom"], 9, 10, 14, 13.5],
29331
+ "text-letter-spacing": ["case", ["==", ["get", "upcase"], true], 0.14, 0],
29332
+ "text-offset": anchorOffset(0.75),
29333
+ "text-anchor": ANCHOR,
29334
+ "text-padding": 6,
29335
+ "text-allow-overlap": false,
29336
+ "symbol-sort-key": 2
29337
+ },
29338
+ paint: { "text-color": T.label, "text-halo-color": T.halo, "text-halo-width": 1.2 }
29339
+ }));
29340
+ layers.push(L({
29341
+ id: "chrtland-summit",
29342
+ type: "symbol",
29343
+ source: src,
29344
+ "source-layer": "land_summit",
29345
+ minzoom: 11.5,
29346
+ filter: ["all", FITS, [">=", ["get", "peak_m"], 40]],
29347
+ layout: {
29348
+ "text-field": ["concat", ["get", "name"], "\n", ["to-string", ["get", "peak_m"]], " m"],
29349
+ "text-font": FONT,
29350
+ "text-size": ["interpolate", ["linear"], ["zoom"], 11.5, 8.5, 14, 11],
29351
+ "text-offset": anchorOffset(0.8),
29352
+ "text-anchor": ANCHOR,
29353
+ "text-padding": 5,
29354
+ "text-allow-overlap": false,
29355
+ "symbol-sort-key": 3
29356
+ },
29357
+ paint: { "text-color": T.label, "text-halo-color": T.halo, "text-halo-width": 1.2 }
29358
+ }));
29359
+ layers.push(L({
29360
+ id: "chrtland-poi",
29361
+ type: "symbol",
29362
+ source: src,
29363
+ "source-layer": "land_poi",
29364
+ minzoom: 13,
29365
+ filter: FITS,
29366
+ layout: {
29367
+ "text-field": ["get", "name"],
29368
+ "text-font": FONT_ITALIC,
29369
+ "text-size": ["interpolate", ["linear"], ["zoom"], 13, 8, 14, 9.5],
29370
+ "text-offset": anchorOffset(0.7),
29371
+ "text-anchor": ANCHOR,
29372
+ "text-padding": 5,
29373
+ "text-allow-overlap": false,
29374
+ "symbol-sort-key": 5
29375
+ },
29376
+ paint: { "text-color": T.label, "text-halo-color": T.halo, "text-halo-width": 1.2 }
29377
+ }));
29378
+ for (const layer of layers) {
29379
+ const l = layer;
29380
+ if (!LAND_SURFACE_LAYERS(l.id)) continue;
29381
+ l.minzoom = Math.max(l.minzoom ?? 0, LAND_SURFACE_MINZOOM);
29382
+ }
29383
+ return layers;
29373
29384
  }
29374
29385
 
29375
29386
  // src/atlas/band-collapse.ts
@@ -30089,7 +30100,7 @@ function generateStyle(palette = "DAY", opts = {}) {
30089
30100
  const surface = [];
30090
30101
  const kept = [];
30091
30102
  assembled.forEach((l, i) => {
30092
- if (i < lastRepaint && isLandGroundOverlay(idOf(l))) surface.push(l);
30103
+ if (i < lastRepaint && isLandOverlay(idOf(l))) surface.push(l);
30093
30104
  else kept.push(l);
30094
30105
  });
30095
30106
  if (surface.length === 0) return assembled;