Commit aaaa7d95 authored by prurite's avatar prurite
Browse files

bus: update vehicle icon

parent 2967e69c
Loading
Loading
Loading
Loading
+4 −4
Changes for docs/.vuepress/components/bus-v2/BusHomeV2.vue: 4 added lines, 4 removed lines.
Original line number Diff line number Diff line
@@ -50,7 +50,7 @@
          <div class="nearby-stop__head"><button type="button" class="link-title" @click="openStop(stop.id)">{{ displayStopName(stop, busLanguage) }}</button><span v-if="stop.distance != null">{{ formatDistance(stop.distance) }}</span></div>
          <p v-if="favoriteArrivals[stop.id]?.loading && !favoriteArrivals[stop.id]?.items?.length" class="muted">{{ busText('loading') }}</p>
          <p v-else-if="favoriteArrivals[stop.id]?.error && !favoriteArrivals[stop.id]?.items?.length" class="muted">{{ busText('unavailable') }}</p>
          <ul v-else class="arrival-list"><li v-for="(arrival, index) in favoriteArrivals[stop.id]?.items" :key="index"><i :style="{ background: arrival.route_color || '#2878c8' }" /><a class="arrival-link" :href="routeDirectionHref(arrival)">{{ arrivalName(arrival) }}</a><span>{{ arrivalText(arrival) }}</span><img v-if="showVehicleIcon(arrival)" class="arrival-vehicle" :src="vehicleIcon(arrival)" :alt="vehicleLabel(arrival)"></li><li v-if="!favoriteArrivals[stop.id]?.items?.length" class="muted">{{ busText('empty') }}</li></ul>
          <ul v-else class="arrival-list"><li v-for="(arrival, index) in favoriteArrivals[stop.id]?.items" :key="index"><i :style="{ background: arrival.route_color || '#2878c8' }" /><a class="arrival-link" :href="routeDirectionHref(arrival)">{{ arrivalName(arrival) }}</a><span>{{ arrivalText(arrival) }}</span><BusVehicleIconV2 v-if="showVehicleIcon(arrival)" :type="arrival.vehicle_type" role="img" :aria-label="vehicleLabel(arrival)" /></li><li v-if="!favoriteArrivals[stop.id]?.items?.length" class="muted">{{ busText('empty') }}</li></ul>
        </article>
        <p v-if="!favoriteRoutes.length && !favoriteStops.length" class="muted">{{ busText('noFavorites') }}</p>
      </section>
@@ -76,7 +76,7 @@
              <i :style="{ background: arrival.route_color || '#2878c8' }" />
              <a class="arrival-link" :href="routeDirectionHref(arrival)">{{ arrivalName(arrival) }}</a>
              <span>{{ arrivalText(arrival) }}</span>
              <img v-if="showVehicleIcon(arrival)" class="arrival-vehicle" :src="vehicleIcon(arrival)" :alt="vehicleLabel(arrival)">
              <BusVehicleIconV2 v-if="showVehicleIcon(arrival)" :type="arrival.vehicle_type" role="img" :aria-label="vehicleLabel(arrival)" />
            </li>
            <li v-if="!arrivals[stop.id]?.items?.length" class="muted">{{ busText('empty') }}</li>
          </ul>
@@ -96,6 +96,7 @@
</template>

<script setup>
import BusVehicleIconV2 from './BusVehicleIconV2.vue'
import { computed, onBeforeUnmount, onMounted, ref } from 'vue'
import { busApi } from './api.mjs'
import { displayName, displayStopName, formatDistance, formatLocalDateTime, haversineMeters, isTerminalArrival, matchesSearch, realtimeArrivalText, sortArrivalsByEstimatedTime, unavailableReasonTextKey } from './core.mjs'
@@ -173,7 +174,6 @@ const noticeTime = (notice) => formatLocalDateTime(notice.starts_at)
const isNoticeOpen = (notice, defaultOpen = false) => noticeIsOpen(noticeOpenStates.value, notice.id, defaultOpen)
const rememberNoticeOpen = (notice, event) => saveNoticeOpenState(noticeOpenStates.value, notice.id, event.target.open)
const arrivalName = (arrival) => `${arrival[busLanguage.value === 'en' ? 'route_name_en' : 'route_name_zh'] || arrival.route_name_zh || arrival.route_name_en || ''} · ${arrival[busLanguage.value === 'en' ? 'direction_name_en' : 'direction_name_zh'] || arrival.direction_name_zh || arrival.direction_name_en || ''}`
const vehicleIcon = (arrival) => String(arrival.vehicle_type).toUpperCase() === 'SHUTTLE' ? '/sev.png' : '/bus.png'
const vehicleLabel = (arrival) => String(arrival.vehicle_type).toUpperCase() === 'SHUTTLE' ? (busLanguage.value === 'zh' ? '电瓶车' : 'EV Shuttle') : (busLanguage.value === 'zh' ? '巴士' : 'Bus')
const showVehicleIcon = (arrival) => !['LAST_SERVICE_PASSED', 'NOT_OPERATING'].includes(String(arrival.unavailable_reason).toUpperCase())

@@ -323,7 +323,7 @@ details + details { border-top: 1px solid var(--bus-v2-border); } summary { disp
.nearby .section-title button { border-color: var(--bus-v2-link-soft); background: var(--bus-v2-link-soft); color: var(--bus-v2-link); font-weight: 600; }
.nearby-stop { padding: .75rem 0; border-top: 1px solid var(--bus-v2-border); } .nearby-stop__head { align-items: baseline; } .link-title { padding: 0; border: 0; background: transparent; color: var(--bus-v2-link); font-weight: 700; text-align: left; }
.nearby-toggle { padding: .4rem .65rem; border: 1px solid var(--bus-v2-border); border-radius: .35rem; background: transparent; color: inherit; font: inherit; }
.arrival-list { margin: .45rem 0 0; padding: 0; list-style: none; } .arrival-list li { display: grid; grid-template-columns: .35rem minmax(0, 1fr) auto 1rem; gap: .4rem; align-items: center; padding: .3rem 0; } .arrival-list i { width: .3rem; height: 1.2rem; border-radius: 2px; } .arrival-vehicle { box-sizing: border-box; width: 1rem; height: 1rem; padding: 1px; border-radius: 50%; background: #fff; } .arrival-link { color: var(--bus-v2-link); text-decoration: none; } .arrival-link:hover, .arrival-link:focus-visible { text-decoration: underline; } .arrival-list .muted { display: block; }
.arrival-list { margin: .45rem 0 0; padding: 0; list-style: none; } .arrival-list li { display: grid; grid-template-columns: .35rem minmax(0, 1fr) auto 1rem; gap: .4rem; align-items: center; padding: .3rem 0; } .arrival-list i { width: .3rem; height: 1.2rem; border-radius: 2px; } .arrival-link { color: var(--bus-v2-link); text-decoration: none; } .arrival-link:hover, .arrival-link:focus-visible { text-decoration: underline; } .arrival-list .muted { display: block; }
.quick-links { display: flex; flex-wrap: wrap; gap: .5rem; } .quick-links button { width: auto; border: 1px solid var(--bus-v2-border); border-radius: .35rem; }
.feature-links { display: grid; grid-template-columns: repeat(2, 1fr); gap: .75rem; } .feature-links a { display: flex; min-height: 4rem; align-items: center; justify-content: center; padding: 0 .75rem; border-radius: .6rem; background: var(--bus-v2-link-soft); color: var(--bus-v2-link); text-align: center; text-decoration: none; font-weight: 600; }
.sr-only { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0,0,0,0); }
+4 −4
Changes for docs/.vuepress/components/bus-v2/BusRouteV2.vue: 4 added lines, 4 removed lines.
Original line number Diff line number Diff line
@@ -59,7 +59,7 @@
        <p v-if="!currentDirection?.stops?.length" class="muted">{{ busText('empty') }}</p>
        <ol v-else>
          <li v-for="stop in currentDirection.stops" :key="stop.id" :class="{ selected: stop.id === selectedStopId }">
            <div class="stop-line"><i aria-hidden="true" /><div v-if="atStop(stop).length || betweenStop(stop).length" class="vehicle-markers"><button v-if="atStop(stop).length" type="button" class="vehicle-marker at" :aria-expanded="expandedVehicleKey === `at-${stop.id}`" @click="toggleVehicles(`at-${stop.id}`)"><b v-if="atStop(stop).length > 1">{{ atStop(stop).length }}</b><i aria-hidden="true" class="vehicle-icon" :class="{ shuttle: String(atStop(stop)[0]?.vehicle_type).toUpperCase() === 'SHUTTLE' }" /><em v-if="hasSpecialService(atStop(stop))">S</em></button><button v-if="betweenStop(stop).length" type="button" class="vehicle-marker between" :aria-expanded="expandedVehicleKey === `between-${stop.id}`" @click="toggleVehicles(`between-${stop.id}`)"><b v-if="betweenStop(stop).length > 1">{{ betweenStop(stop).length }}</b><i aria-hidden="true" class="vehicle-icon" :class="{ shuttle: String(betweenStop(stop)[0]?.vehicle_type).toUpperCase() === 'SHUTTLE' }" /><em v-if="hasSpecialService(betweenStop(stop))">S</em></button></div></div>
            <div class="stop-line"><i aria-hidden="true" /><div v-if="atStop(stop).length || betweenStop(stop).length" class="vehicle-markers"><button v-if="atStop(stop).length" type="button" class="vehicle-marker at" :aria-expanded="expandedVehicleKey === `at-${stop.id}`" @click="toggleVehicles(`at-${stop.id}`)"><b v-if="atStop(stop).length > 1">{{ atStop(stop).length }}</b><BusVehicleIconV2 aria-hidden="true" class="vehicle-icon" :type="atStop(stop)[0]?.vehicle_type" /><em v-if="hasSpecialService(atStop(stop))">S</em></button><button v-if="betweenStop(stop).length" type="button" class="vehicle-marker between" :aria-expanded="expandedVehicleKey === `between-${stop.id}`" @click="toggleVehicles(`between-${stop.id}`)"><b v-if="betweenStop(stop).length > 1">{{ betweenStop(stop).length }}</b><BusVehicleIconV2 aria-hidden="true" class="vehicle-icon" :type="betweenStop(stop)[0]?.vehicle_type" /><em v-if="hasSpecialService(betweenStop(stop))">S</em></button></div></div>
            <div class="stop-main"><a :href="`${stopHref}${encodeURIComponent(stop.id)}`" @click.prevent="selectStop(stop.id)"><strong>{{ routeStopName(stop) }}</strong></a></div>
            <div v-if="expandedVehicleKey === `at-${stop.id}` || expandedVehicleKey === `between-${stop.id}`" class="vehicle-details">
              <BusVehicleDetailV2 v-for="vehicle in expandedVehicleKey === `at-${stop.id}` ? atStop(stop) : betweenStop(stop)" :key="vehicle.id" :vehicle="vehicle" :routes="[route]" :stops="stops" :language="busLanguage" />
@@ -78,6 +78,7 @@
</template>

<script setup>
import BusVehicleIconV2 from './BusVehicleIconV2.vue'
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'
import { busApi, publicApi } from './api.mjs'
import { displayName, displayStopName, formatLocalDateTime, realtimeArrivalText, serviceTypeLabel, unavailableReasonTextKey } from './core.mjs'
@@ -154,15 +155,14 @@ defineExpose({ load, refresh, selectDirection, selectStop })
.notices summary time { color: #687386; font-size: .8rem; font-weight: 400; white-space: nowrap; }
.bus-detail { max-width: 900px; margin: 0 auto; color: var(--c-text, #243043); } .detail-head, .head-actions, .section-head, .route-summary { display: flex; align-items: center; gap: .75rem; } .detail-head, .section-head { justify-content: space-between; } .detail-head h1, .panel h3, .map-slot > h3 { margin: 0; padding-top: 0; } .detail-head h1 { font-size: 1.4rem; line-height: 1.25; } p { margin-top: 0; } .panel h3, .map-slot > h3 { font-size: 1rem; line-height: 1.3; } .eyebrow { margin-bottom: .2rem; color: #687386; font-size: .82rem; } .head-actions { align-self: flex-start; } .plain-button { border: 1px solid #aec5dc; border-radius: .35rem; padding: .4rem .65rem; background: transparent; color: inherit; font: inherit; cursor: pointer; } .panel { margin: 1rem 0; padding: 1rem; border: 1px solid var(--c-border, #dce2ea); border-radius: .6rem; background: var(--c-bg-soft, #fff); } .status { display: flex; align-items: center; gap: .5rem; } .error { color: #a32727; } .spinner { width: 1em; height: 1em; border: 2px solid #b7c9dd; border-top-color: #2672bc; border-radius: 50%; animation: spin .8s linear infinite; } @keyframes spin { to { transform: rotate(360deg); } } .route-summary { flex-wrap: wrap; padding-top: .5rem; padding-bottom: .5rem; border-left: .5rem solid var(--route-color); justify-content: space-between; } .route-summary div { display: flex; flex-wrap: wrap; min-width: 0; align-items: baseline; gap: .5rem; } .route-swatch { width: .9rem; height: .9rem; border-radius: 50%; background: var(--route-color); } .direction-tabs { display: flex; flex-wrap: wrap; gap: .5rem; margin: 1rem 0; } .direction-tabs button { border: 1px solid #cbd9e7; border-radius: 999px; padding: .45rem .8rem; background: transparent; color: inherit; font: inherit; cursor: pointer; } .direction-tabs .active { border-color: var(--c-brand, #2878c8); background: #eaf3fc; color: #175f9f; font-weight: 700; } .muted { color: #687386; } .selected-stop-title, .arrival-list li { display: flex; flex-wrap: wrap; align-items: baseline; gap: .2rem .5rem; } .selected-stop-title > *, .arrival-list li > * { white-space: nowrap; } .arrival-list { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: .5rem; margin: .5rem 0; padding: 0; list-style: none; } .arrival-list li { padding: .55rem; border: 1px solid #e7edf4; border-radius: .35rem; } .arrival-list small { color: #687386; } .all-link { display: inline-block; margin-top: .75rem; color: #1765ac; } details + details { border-top: 1px solid #e7edf4; } summary { padding: .65rem 0; cursor: pointer; font-weight: 600; } .markdown { margin: 1rem 0 0; padding-bottom: .25rem; } .markdown :deep(p) { margin: 1rem 0 0; } .markdown :deep(:first-child) { margin-top: 0; } .markdown :deep(:last-child) { margin-bottom: 0; } .route-stops { max-height: 18rem; overflow-y: auto; } .route-stops ol { margin: 0; padding: 0; list-style: none; } .route-stops li { position: relative; display: grid; grid-template-columns: 1.6rem minmax(0, 1fr); min-height: 2.25rem; } .stop-line { position: relative; display: flex; justify-content: center; } .stop-line::before { content: ''; position: absolute; z-index: 0; top: 0; bottom: 0; width: 4px; background: var(--route-color); } .stop-line > i { z-index: 1; width: .75rem; height: .75rem; margin-top: .65rem; border: 3px solid var(--route-color); border-radius: 50%; background: #fff; } .selected .stop-line > i { background: var(--route-color); box-shadow: 0 0 0 4px color-mix(in srgb, var(--route-color) 25%, transparent); } .stop-main { padding: .25rem .25rem .25rem .5rem; } .stop-main a { color: inherit; font: inherit; text-decoration: none; } .stop-main a:hover, .stop-main a:focus-visible { color: var(--route-color); text-decoration: underline; } .selected .stop-main strong { color: var(--route-color); } .vehicle-markers { position: absolute; z-index: 2; inset: 0; pointer-events: none; } .vehicle-marker { position: absolute; left: 50%; transform: translateX(-50%); border: 0; border-radius: 999px; padding: .2rem .3rem; background: #e8f2fb; font: inherit; font-size: .78rem; cursor: pointer; pointer-events: auto; white-space: nowrap; } .vehicle-marker.at { top: .25rem; } .vehicle-marker.between { top: 2.1rem; background: #fff4d9; } .vehicle-marker em { margin-left: .1rem; color: #9a5200; font-size: .7rem; font-style: normal; font-weight: 800; } .vehicle-marker b { margin-left: .15rem; } .vehicle-details { display: grid; grid-column: 2; gap: .4rem; margin: 0 .5rem .6rem; } .vehicle-details :deep(.vehicle-detail) { box-sizing: border-box; min-width: 0; box-shadow: none; } .map-slot { margin: 1rem 0; } @media (max-width: 560px) { .detail-head { align-items: flex-start; } .head-actions { flex-wrap: wrap; justify-content: flex-end; } .panel { margin: .75rem 0; } .arrival-list { grid-template-columns: 1fr; } }
.route-stops .stop-line > i { background: var(--bus-v2-bg); }
.route-stops .vehicle-marker { transform: translate(-63%, -50%); background: var(--bus-v2-link-soft); color: var(--bus-v2-text); }
.route-stops .vehicle-marker { transform: translate(-64%, -50%); background: var(--bus-v2-link-soft); color: var(--bus-v2-text); }
.route-stops .vehicle-marker.at { top: 1rem; }
.route-stops .vehicle-marker.between { top: -0.1rem; background: color-mix(in srgb, #f7a600 18%, var(--bus-v2-bg)); }
.route-stops .vehicle-marker { gap: .1rem; padding: 0; background: transparent; }
.route-stops .vehicle-marker { display: flex; align-items: center; }
.route-stops .vehicle-marker b { margin: 0 .15rem 0 0; }
.route-stops .vehicle-marker.between { background: transparent; }
.route-stops .vehicle-icon { z-index: auto; display: block; width: 1.5rem; height: 1.5rem; margin: 0; border-radius: 50%; background: #fff url('/bus.png') center / calc(100% - 2px) no-repeat; }
.route-stops .vehicle-icon.shuttle { background-image: url('/sev.png'); }
.route-stops .vehicle-icon { display: block; width: 1.2rem; height: 1.2rem; }
.head-actions { gap: .3rem; }
.plain-button, .selected-stop-title a { color: inherit; text-decoration: none; }
.selected-stop-title a:hover, .selected-stop-title a:focus-visible { color: var(--bus-v2-link); text-decoration: underline; }
+2 −3
Changes for docs/.vuepress/components/bus-v2/BusStopArrivalsV2.vue: 2 added lines, 3 removed lines.
Original line number Diff line number Diff line
@@ -2,10 +2,11 @@
  <p v-if="props.state?.loading && !props.state?.items?.length" class="muted">{{ busText('loading') }}</p>
  <p v-else-if="props.state?.error && !props.state?.items?.length" class="muted">{{ busText('unavailable') }}</p>
  <p v-else-if="!items.length" class="muted">{{ busText('empty') }}</p>
  <ul v-else class="arrival-list"><li v-for="(arrival, index) in items" :key="index"><i :style="{ background: arrival.route_color || '#2878c8' }" /><a :href="routeDirectionHref(arrival)"><strong>{{ routeName(arrival) }}</strong><small>{{ directionName(arrival) }}</small></a><span>{{ arrivalText(arrival) }}<small v-if="arrivalMeta(arrival)">{{ arrivalMeta(arrival) }}</small></span><img v-if="showVehicleIcon(arrival)" :src="vehicleIcon(arrival)" :alt="vehicleLabel(arrival)"></li></ul>
  <ul v-else class="arrival-list"><li v-for="(arrival, index) in items" :key="index"><i :style="{ background: arrival.route_color || '#2878c8' }" /><a :href="routeDirectionHref(arrival)"><strong>{{ routeName(arrival) }}</strong><small>{{ directionName(arrival) }}</small></a><span>{{ arrivalText(arrival) }}<small v-if="arrivalMeta(arrival)">{{ arrivalMeta(arrival) }}</small></span><BusVehicleIconV2 v-if="showVehicleIcon(arrival)" :type="arrival.vehicle_type" role="img" :aria-label="vehicleLabel(arrival)" /></li></ul>
</template>

<script setup>
import BusVehicleIconV2 from './BusVehicleIconV2.vue'
import { computed } from 'vue'
import { closestArrivalsByRoute, realtimeArrivalText, unavailableReasonTextKey } from './core.mjs'
import { busLanguage, busText } from './i18n.mjs'
@@ -16,7 +17,6 @@ const routeName = (item) => item[busLanguage.value === 'en' ? 'route_name_en' :
const directionName = (item) => item[busLanguage.value === 'en' ? 'direction_name_en' : 'direction_name_zh'] || item.direction_name_zh || item.direction_name_en || ''
const arrivalKey = (item) => `${item.route_direction_id}-${item.trip_id || item.planned_arrival_at || item.updated_at || item.eta_minutes}`
const routeDirectionHref = (item) => `${props.routeHref}${encodeURIComponent(item.route_id)}&direction=${encodeURIComponent(item.route_direction_id)}`
const vehicleIcon = (item) => String(item.vehicle_type).toUpperCase() === 'SHUTTLE' ? '/sev.png' : '/bus.png'
const vehicleLabel = (item) => String(item.vehicle_type).toUpperCase() === 'SHUTTLE' ? (busLanguage.value === 'zh' ? '电瓶车' : 'EV Shuttle') : (busLanguage.value === 'zh' ? '巴士' : 'Bus')
const showVehicleIcon = (item) => !['LAST_SERVICE_PASSED', 'NOT_OPERATING'].includes(String(item.unavailable_reason).toUpperCase())
function time(value) { return value ? new Date(value).toLocaleTimeString(busLanguage.value === 'zh' ? 'zh-CN' : 'en', { hour: '2-digit', minute: '2-digit', hour12: false }) : '' }
@@ -34,5 +34,4 @@ function arrivalMeta(item) { return item.source === 'real_time' ? `${busLanguage
.arrival-list a:hover, .arrival-list a:focus-visible { text-decoration: underline; }
.arrival-list small { color: var(--bus-v2-muted); }
.arrival-list span { display: grid; justify-items: end; gap: .15rem; text-align: right; }
.arrival-list img { box-sizing: border-box; width: 1rem; height: 1rem; padding: 1px; border-radius: 50%; background: #fff; }
</style>
+12 −0
Changes for docs/.vuepress/components/bus-v2/BusVehicleIconV2.vue: 12 added lines, 0 removed lines.
Original line number Diff line number Diff line
<template>
  <svg class="bus-vehicle-icon" :class="{ shuttle: String(type).toUpperCase() === 'SHUTTLE' }" viewBox="0 0 16 16"><circle cx="8" cy="8" r="7.5" /></svg>
</template>

<script setup>
defineProps({ type: String })
</script>

<style scoped>
.bus-vehicle-icon { display: inline-block; flex-shrink: 0; width: .8rem; height: .8rem; fill: var(--bus-v2-bus-fill); stroke: #925a00; }
.bus-vehicle-icon.shuttle { fill: var(--bus-v2-shuttle-fill); stroke: #28633f; }
</style>
+3 −3
Changes for docs/.vuepress/components/bus-v2/BusVehicleLegendV2.vue: 3 added lines, 3 removed lines.
Original line number Diff line number Diff line
<template>
  <div class="vehicle-legend" :aria-label="language === 'zh' ? '车型图例' : 'Vehicle legend'">
    <span><i v-if="textMarkers" class="vehicle-legend__marker" :style="{ color: vehicleTextColor('BUS') }" aria-hidden="true">1</i><img v-else src="/bus.png" alt="">{{ language === 'zh' ? '巴士' : 'Bus' }}</span>
    <span><i v-if="textMarkers" class="vehicle-legend__marker shuttle" :style="{ color: vehicleTextColor('SHUTTLE') }" aria-hidden="true">1</i><img v-else src="/sev.png" alt="">{{ language === 'zh' ? '电瓶车' : 'EV Shuttle' }}</span>
    <span><i v-if="textMarkers" class="vehicle-legend__marker" :style="{ color: vehicleTextColor('BUS') }" aria-hidden="true">1</i><BusVehicleIconV2 v-else type="BUS" aria-hidden="true" />{{ language === 'zh' ? '巴士' : 'Bus' }}</span>
    <span><i v-if="textMarkers" class="vehicle-legend__marker shuttle" :style="{ color: vehicleTextColor('SHUTTLE') }" aria-hidden="true">1</i><BusVehicleIconV2 v-else type="SHUTTLE" aria-hidden="true" />{{ language === 'zh' ? '电瓶车' : 'EV Shuttle' }}</span>
  </div>
</template>

<script setup>
import BusVehicleIconV2 from './BusVehicleIconV2.vue'
import { vehicleTextColor } from './core.mjs'
defineProps({ language: { type: String, default: 'zh' }, textMarkers: Boolean })
</script>

<style scoped>
.vehicle-legend { display: flex; flex-wrap: wrap; gap: .6rem; margin-top: .5rem; color: var(--bus-v2-muted); font-size: .82rem; }.vehicle-legend span { display: inline-flex; align-items: center; gap: .25rem; white-space: nowrap; }
.vehicle-legend img { box-sizing: border-box; width: 1rem; height: 1rem; padding: 1px; border-radius: 50%; background: #fff; }
.vehicle-legend__marker { display: inline-flex; align-items: center; justify-content: center; width: 1.25rem; height: 1.25rem; border-radius: 50%; background: var(--bus-v2-bus-fill); font: 700 .75rem/1 "Microsoft YaHei UI", "Microsoft YaHei", "PingFang SC", sans-serif; }
.vehicle-legend__marker.shuttle { background: var(--bus-v2-shuttle-fill); }
</style>