Migrate the regions pages onto portedGlobals, and fix a Cyrillic rule that was inert

upg100's harness recorded my two files as NOT MEASURED because its stubs threw on
them. Both were FIDELITY gaps — the stub modelled states that cannot occur — so
they are fixed by making the stub more like the browser, never by relaxing a
comparison:

  · `.value` was undefined, so a component reading an INJECTED ref threw
    "Cannot read properties of undefined" (region-dashboards.vue:199,
    cfg.value.regionSlug — ProfileLoader always provides that object).
  · the stub threw on ANY string coercion, because a Proxy over a function has no
    primitive conversion. That alone produced regions-list's "Cannot convert
    object to primitive value".

`.value` is now the EMPTY STRING, which is the one shape that serves both real
cases honestly: a locale ref's .value IS a string, so `${locale.value || 'en'}`
correctly yields 'en'; and reading a property off '' returns undefined instead of
throwing, so an injected-config read falls through its own `|| ''` exactly as in
the browser. Symbol.toPrimitive makes any remaining stub render empty.
Measured components: 2 -> 4, with no NOT MEASURED left.

★ AND THE MIGRATION FOUND A REAL BUG, which is the part worth reading.
regions-list's Cyrillic heading rule ended with a trailing comma and NO
declaration block, so the concatenation emitted:

    [lang="ru"] .rl-title, [lang="ru-RU"] .rl-title,regions-list

A selector list with nothing to apply — the Russian heading font was silently
never applied — AND it swallowed useShadowStyles' style-id argument as CSS,
leaving that sheet unlabelled. Braces stayed balanced, so
check:shadow-css-parses could not see it, and it passed 80/80 both before and
after. cyrillicHeading('.rl-title') replaces it and emits a real rule.

SO THE BYTE-COMPARE SPLITS, deliberately, and I did not paper over it:
  region-dashboards.vue  IDENTICAL — cyrillicHeading('.rd-title','.rd-h2')
                         emits exactly the literal it replaced, verified
                         character-for-character before the swap. A pure
                         de-duplication.
  regions-list.vue       CHANGED, and the change IS the fix. The old bytes were
                         the bug, so its baseline is re-taken rather than
                         preserved. Preserving it would have pinned a broken
                         rule forever, which is the one thing a byte-identical
                         gate must not be used for.

NOT MIGRATED, on purpose: strongMedium(). The helper emits bare `strong{...}`
while region-dashboards emits `.rd-root strong{...}` — a narrower selector.
Swapping it would widen the rule to every <strong> in the shadow root, a
behaviour change disguised as a de-duplication. scrollbarGutter() is likewise
left inline: both files carry it inside a larger .rd-scroll/.rl-scroll rule, not
as a standalone declaration the helper could replace.

Both files now gated (owned:true) instead of recorded-only, so a future edit that
changes their emitted CSS fails rather than being noted.
This commit is contained in:
Doxa Maps 2026-09-29 02:15:00 +00:00
parent 0428f279c1
commit 4a4bc96fca
10 changed files with 82 additions and 51 deletions

View file

@ -60,8 +60,8 @@ const BASELINE = join(HERE, 'fixtures', 'ported-globals-baseline.json')
const FILES = [
{ path: 'library/components/regions/GoalCard.vue', owned: true },
{ path: 'library/components/regions/RegionSectionCard.vue', owned: true },
{ path: 'plugin-designs/regions-dashboard/region-dashboards.vue', owned: false },
{ path: 'plugin-designs/regions-dashboard/regions-list.vue', owned: false },
{ path: 'plugin-designs/regions-dashboard/region-dashboards.vue', owned: true },
{ path: 'plugin-designs/regions-dashboard/regions-list.vue', owned: true },
]
const OWNED = new Set(FILES.filter((f) => f.owned).map((f) => f.path))
@ -106,8 +106,27 @@ async function emittedCss(src, rel) {
// A stub that tolerates any shape a component might use it in: called, constructed, or
// property-accessed. defineProps/defineEmits are compiler macros, so they must return something
// indexable rather than undefined.
// ★ TWO STUB GAPS CLOSED, and both were FIDELITY problems rather than assertions to weaken.
// The stub modelled states that cannot occur in real use, so two real components could not be
// measured at all and were recorded as NOT MEASURED:
// · `.value` was undefined. A component reading an INJECTED ref — cfg.value.regionSlug on
// region-dashboards.vue:199, where ProfileLoader always provides a profile-config object —
// threw "Cannot read properties of undefined".
// · the stub itself threw on any STRING COERCION (template interpolation, String(), + ''),
// because a Proxy over a function has no primitive conversion. regions-list threw "Cannot
// convert object to primitive value" for that reason alone.
// `.value` is now the EMPTY STRING, which is the one shape that serves both real cases honestly:
// a locale ref's .value IS a string, so `${locale.value || 'en'}` correctly yields 'en'; and
// reading a property off '' returns undefined instead of throwing, so an injected-config read
// falls through its own `|| ''` exactly as it does in the browser. Symbol.toPrimitive makes any
// remaining stub render as empty rather than explode. Nothing here relaxes a comparison — the
// emitted CSS is still hashed byte-for-byte.
const anyFn = () => new Proxy(function () {}, {
get: (t, k) => (k === 'value' ? undefined : anyFn()),
get: (t, k) => {
if (k === 'value') return ''
if (k === Symbol.toPrimitive) return () => ''
return anyFn()
},
apply: () => anyFn(),
})
const stubModule = (spec) => new Proxy({}, {

View file

@ -1,8 +1,10 @@
{
"_why": "Emitted-CSS hashes, captured by EXECUTING each component's setup and hashing the string it hands useShadowStyles. Whitespace-normalised; declarations and rule grouping are not. A ported-globals extraction must not change these.",
"_source": "git ref 45e31f102",
"_source": "working tree",
"hashes": {
"library/components/regions/GoalCard.vue": "987d94b1cd66110d",
"library/components/regions/RegionSectionCard.vue": "897d948b17b21efe"
"library/components/regions/RegionSectionCard.vue": "897d948b17b21efe",
"plugin-designs/regions-dashboard/region-dashboards.vue": "ea1d2047307b1efd",
"plugin-designs/regions-dashboard/regions-list.vue": "ab4877b933e5c8a3"
}
}

View file

@ -59,6 +59,7 @@ import BackToTopButton from '@map/components/BackToTopButton.vue'
import { SHADOW_BOX_SIZING_RESET } from './src/shadowReset.js'
import { summarizeRegions, emptyStats, toNumber, PEOPLE_GROUP_LIST_FIELDS } from '@map/utils/countries-meta.js'
import { peopleGroupPoints, PEOPLE_GROUP_POINT_FIELDS } from '@map/utils/peopleGroupPoints.js'
import { cyrillicHeading } from '@map/styles/portedGlobals.js'
const props = defineProps({
profileConfig: { type: String, default: '' },
@ -1004,9 +1005,11 @@ useShadowStyles(
// BOTH TAG FORMS, deliberately: their site tags the document ru-RU while our i18n locale is `ru`,
// and a host page embedding this element may set either. A selector that matched only one would
// work on their site and fail on ours, or the reverse, depending on who set the attribute.
'[lang="ru"] .rd-title, [lang="ru-RU"] .rd-title,' +
'[lang="ru"] .rd-h2, [lang="ru-RU"] .rd-h2{ font-family:"Oswald","Arial Narrow",BlinkMacSystemFont,"Segoe UI","Helvetica Neue",Arial,sans-serif;' +
' letter-spacing:-0.02em; }' +
// The Cyrillic heading face now comes from the shared module rather than a third copy of the
// same rule — its output is byte-identical to what this file emitted before (verified), so this
// is a de-duplication and not a style change. See library/styles/portedGlobals.js for the
// measurement history and for the one global it DELIBERATELY does not port.
cyrillicHeading('.rd-title', '.rd-h2') +
// .rd-big / .rd-big-label / .rd-goal and the .stack--xs that separates a bar from its legend are
// in library/components/regions/GoalCard.vue and library/components/charts/ProgressBar.vue now,
// together with the measurement history that fixed each value. The `strong` rule below STAYS:

View file

@ -69,6 +69,7 @@ import RegionSectionCard from '@map/components/regions/RegionSectionCard.vue'
import posterData from '@map/data/country-posters.json'
import { resolveAssetsBase } from '@map/utils/embedBase.js'
import { onBeforeUnmount } from 'vue'
import { cyrillicHeading } from '@map/styles/portedGlobals.js'
const props = defineProps({
profileConfig: { type: String, default: '' },
@ -447,7 +448,13 @@ useShadowStyles(
// BOTH TAG FORMS, deliberately: their site tags the document ru-RU while our i18n locale is `ru`,
// and a host page embedding this element may set either. A selector that matched only one would
// work on their site and fail on ours, or the reverse, depending on who set the attribute.
'[lang="ru"] .rl-title, [lang="ru-RU"] .rl-title,' +
// ★ THIS RULE WAS INERT, AND THE LABEL WAS BEING EATEN. The line above used to end with a
// trailing comma and no declaration block, so the concatenation produced
// [lang="ru"] .rl-title, [lang="ru-RU"] .rl-title,regions-list
// — a selector list with nothing to apply, which silently dropped the Cyrillic heading font AND
// consumed useShadowStyles' style-id argument as CSS, leaving the sheet unlabelled. Braces stayed
// balanced, so check:shadow-css-parses could not see it. Found while migrating to portedGlobals.
cyrillicHeading('.rl-title'),
'regions-list'
)
</script>

View file

@ -1108,7 +1108,7 @@
"bundle": "regions-dashboard",
"scriptUrl": "./regions-dashboard/regions-dashboard.js",
"output": "dx-maps/regions-dashboard/regions-dashboard.js",
"integrity": "sha384-Qx++MjP11JrPOOY3YfEA7zk1oI50A3w/uKfVbMmcPx8ovEZ04AbApKWmWV4O0OYo",
"integrity": "sha384-/rKThVpoexbRuzepfBMP78XR/Roq/t8XDDGYu21/MAyhyaq0RFDGIe903d52gSzo",
"elements": [
"regions-dashboard"
],
@ -1131,7 +1131,7 @@
"scriptUrl": "./regions-dashboard/regions-dashboard.js",
"embed": {
"iframe": "<iframe src=\"./regions-dashboard/region-dashboards/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>",
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./regions-dashboard/regions-dashboard.js\" integrity=\"sha384-Qx++MjP11JrPOOY3YfEA7zk1oI50A3w/uKfVbMmcPx8ovEZ04AbApKWmWV4O0OYo\" crossorigin=\"anonymous\"></script>\n<!-- provider: \"maplibre\" is the default; change it to \"mapbox\" and fill tk to use Mapbox. -->\n<regions-dashboard profile-config='{\"profile\":\"region-dashboards\",\"provider\":\"maplibre\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></regions-dashboard>"
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./regions-dashboard/regions-dashboard.js\" integrity=\"sha384-/rKThVpoexbRuzepfBMP78XR/Roq/t8XDDGYu21/MAyhyaq0RFDGIe903d52gSzo\" crossorigin=\"anonymous\"></script>\n<!-- provider: \"maplibre\" is the default; change it to \"mapbox\" and fill tk to use Mapbox. -->\n<regions-dashboard profile-config='{\"profile\":\"region-dashboards\",\"provider\":\"maplibre\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></regions-dashboard>"
}
},
{
@ -1147,7 +1147,7 @@
"scriptUrl": "./regions-dashboard/regions-dashboard.js",
"embed": {
"iframe": "<iframe src=\"./regions-dashboard/regions-list/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>",
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./regions-dashboard/regions-dashboard.js\" integrity=\"sha384-Qx++MjP11JrPOOY3YfEA7zk1oI50A3w/uKfVbMmcPx8ovEZ04AbApKWmWV4O0OYo\" crossorigin=\"anonymous\"></script>\n<!-- provider: \"maplibre\" is the default; change it to \"mapbox\" and fill tk to use Mapbox. -->\n<regions-dashboard profile-config='{\"profile\":\"regions-list\",\"provider\":\"maplibre\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></regions-dashboard>"
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./regions-dashboard/regions-dashboard.js\" integrity=\"sha384-/rKThVpoexbRuzepfBMP78XR/Roq/t8XDDGYu21/MAyhyaq0RFDGIe903d52gSzo\" crossorigin=\"anonymous\"></script>\n<!-- provider: \"maplibre\" is the default; change it to \"mapbox\" and fill tk to use Mapbox. -->\n<regions-dashboard profile-config='{\"profile\":\"regions-list\",\"provider\":\"maplibre\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></regions-dashboard>"
}
},
{
@ -1163,7 +1163,7 @@
"scriptUrl": "./regions-dashboard/regions-dashboard.js",
"embed": {
"iframe": "<iframe src=\"./regions-dashboard/regions-mfe/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>",
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./regions-dashboard/regions-dashboard.js\" integrity=\"sha384-Qx++MjP11JrPOOY3YfEA7zk1oI50A3w/uKfVbMmcPx8ovEZ04AbApKWmWV4O0OYo\" crossorigin=\"anonymous\"></script>\n<!-- provider: \"maplibre\" is the default; change it to \"mapbox\" and fill tk to use Mapbox. -->\n<regions-dashboard profile-config='{\"profile\":\"regions-mfe\",\"provider\":\"maplibre\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></regions-dashboard>"
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./regions-dashboard/regions-dashboard.js\" integrity=\"sha384-/rKThVpoexbRuzepfBMP78XR/Roq/t8XDDGYu21/MAyhyaq0RFDGIe903d52gSzo\" crossorigin=\"anonymous\"></script>\n<!-- provider: \"maplibre\" is the default; change it to \"mapbox\" and fill tk to use Mapbox. -->\n<regions-dashboard profile-config='{\"profile\":\"regions-mfe\",\"provider\":\"maplibre\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></regions-dashboard>"
}
}
]

View file

@ -1108,7 +1108,7 @@
"bundle": "regions-dashboard",
"scriptUrl": "./regions-dashboard/regions-dashboard.js",
"output": "dx-maps/regions-dashboard/regions-dashboard.js",
"integrity": "sha384-Qx++MjP11JrPOOY3YfEA7zk1oI50A3w/uKfVbMmcPx8ovEZ04AbApKWmWV4O0OYo",
"integrity": "sha384-/rKThVpoexbRuzepfBMP78XR/Roq/t8XDDGYu21/MAyhyaq0RFDGIe903d52gSzo",
"elements": [
"regions-dashboard"
],
@ -1131,7 +1131,7 @@
"scriptUrl": "./regions-dashboard/regions-dashboard.js",
"embed": {
"iframe": "<iframe src=\"./regions-dashboard/region-dashboards/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>",
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./regions-dashboard/regions-dashboard.js\" integrity=\"sha384-Qx++MjP11JrPOOY3YfEA7zk1oI50A3w/uKfVbMmcPx8ovEZ04AbApKWmWV4O0OYo\" crossorigin=\"anonymous\"></script>\n<!-- provider: \"maplibre\" is the default; change it to \"mapbox\" and fill tk to use Mapbox. -->\n<regions-dashboard profile-config='{\"profile\":\"region-dashboards\",\"provider\":\"maplibre\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></regions-dashboard>"
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./regions-dashboard/regions-dashboard.js\" integrity=\"sha384-/rKThVpoexbRuzepfBMP78XR/Roq/t8XDDGYu21/MAyhyaq0RFDGIe903d52gSzo\" crossorigin=\"anonymous\"></script>\n<!-- provider: \"maplibre\" is the default; change it to \"mapbox\" and fill tk to use Mapbox. -->\n<regions-dashboard profile-config='{\"profile\":\"region-dashboards\",\"provider\":\"maplibre\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></regions-dashboard>"
}
},
{
@ -1147,7 +1147,7 @@
"scriptUrl": "./regions-dashboard/regions-dashboard.js",
"embed": {
"iframe": "<iframe src=\"./regions-dashboard/regions-list/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>",
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./regions-dashboard/regions-dashboard.js\" integrity=\"sha384-Qx++MjP11JrPOOY3YfEA7zk1oI50A3w/uKfVbMmcPx8ovEZ04AbApKWmWV4O0OYo\" crossorigin=\"anonymous\"></script>\n<!-- provider: \"maplibre\" is the default; change it to \"mapbox\" and fill tk to use Mapbox. -->\n<regions-dashboard profile-config='{\"profile\":\"regions-list\",\"provider\":\"maplibre\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></regions-dashboard>"
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./regions-dashboard/regions-dashboard.js\" integrity=\"sha384-/rKThVpoexbRuzepfBMP78XR/Roq/t8XDDGYu21/MAyhyaq0RFDGIe903d52gSzo\" crossorigin=\"anonymous\"></script>\n<!-- provider: \"maplibre\" is the default; change it to \"mapbox\" and fill tk to use Mapbox. -->\n<regions-dashboard profile-config='{\"profile\":\"regions-list\",\"provider\":\"maplibre\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></regions-dashboard>"
}
},
{
@ -1163,7 +1163,7 @@
"scriptUrl": "./regions-dashboard/regions-dashboard.js",
"embed": {
"iframe": "<iframe src=\"./regions-dashboard/regions-mfe/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>",
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./regions-dashboard/regions-dashboard.js\" integrity=\"sha384-Qx++MjP11JrPOOY3YfEA7zk1oI50A3w/uKfVbMmcPx8ovEZ04AbApKWmWV4O0OYo\" crossorigin=\"anonymous\"></script>\n<!-- provider: \"maplibre\" is the default; change it to \"mapbox\" and fill tk to use Mapbox. -->\n<regions-dashboard profile-config='{\"profile\":\"regions-mfe\",\"provider\":\"maplibre\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></regions-dashboard>"
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./regions-dashboard/regions-dashboard.js\" integrity=\"sha384-/rKThVpoexbRuzepfBMP78XR/Roq/t8XDDGYu21/MAyhyaq0RFDGIe903d52gSzo\" crossorigin=\"anonymous\"></script>\n<!-- provider: \"maplibre\" is the default; change it to \"mapbox\" and fill tk to use Mapbox. -->\n<regions-dashboard profile-config='{\"profile\":\"regions-mfe\",\"provider\":\"maplibre\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></regions-dashboard>"
}
}
]

View file

@ -61,7 +61,7 @@
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
<!-- Bundle IIFE — registers the <regions-dashboard> custom element. -->
<script src="../regions-dashboard.js" integrity="sha384-Qx++MjP11JrPOOY3YfEA7zk1oI50A3w/uKfVbMmcPx8ovEZ04AbApKWmWV4O0OYo" crossorigin="anonymous"></script>
<script src="../regions-dashboard.js" integrity="sha384-/rKThVpoexbRuzepfBMP78XR/Roq/t8XDDGYu21/MAyhyaq0RFDGIe903d52gSzo" crossorigin="anonymous"></script>
<script>
// Mapbox token resolution — the A3 ladder, in precedence order:

File diff suppressed because one or more lines are too long

View file

@ -61,7 +61,7 @@
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
<!-- Bundle IIFE — registers the <regions-dashboard> custom element. -->
<script src="../regions-dashboard.js" integrity="sha384-Qx++MjP11JrPOOY3YfEA7zk1oI50A3w/uKfVbMmcPx8ovEZ04AbApKWmWV4O0OYo" crossorigin="anonymous"></script>
<script src="../regions-dashboard.js" integrity="sha384-/rKThVpoexbRuzepfBMP78XR/Roq/t8XDDGYu21/MAyhyaq0RFDGIe903d52gSzo" crossorigin="anonymous"></script>
<script>
// Mapbox token resolution — the A3 ladder, in precedence order:

View file

@ -72,7 +72,7 @@
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
<!-- Bundle IIFE — registers the <regions-dashboard> custom element. -->
<script src="../regions-dashboard.js" integrity="sha384-Qx++MjP11JrPOOY3YfEA7zk1oI50A3w/uKfVbMmcPx8ovEZ04AbApKWmWV4O0OYo" crossorigin="anonymous"></script>
<script src="../regions-dashboard.js" integrity="sha384-/rKThVpoexbRuzepfBMP78XR/Roq/t8XDDGYu21/MAyhyaq0RFDGIe903d52gSzo" crossorigin="anonymous"></script>
<script>
// Mapbox token resolution — the A3 ladder, in precedence order: