fix(brand): the logo, the typeface and the palette are HDFC's actual ones

The console was wearing an approximation of the bank. Everything here was
read off HDFC Bank's own production assets instead.

The mark was inside out. It had been drawn as a blue square with a red
square inset; HDFC's is the reverse — a red ground, a white channel cross
splitting it into four corner blocks, and a small blue square at the
centre. Every coordinate now comes verbatim from the SVG the bank serves
in its own site header, including the "HDFC BANK" wordmark outlines, so
the lockup is the bank's rather than a redrawing of it. The favicon in
index.html carried the same inversion and is fixed with the same numbers.

Open Sans replaces the Segoe UI stack. hdfcbank.com preloads OpenSans
Regular / SemiBold / Bold / ExtraBold and sets its whole site in it. It is
self-hosted through @fontsource-variable/open-sans rather than pulled from
a CDN, for the reason the API URL is read at runtime: this build gets
promoted between environments and dropped behind bank proxies, and a font
that sometimes fails to arrive is a product that sometimes looks like
someone else's.

The header stops being a navy band. HDFC puts the full lockup on a light
ground on every one of its own digital surfaces, and a navy header made
the real logo unusable — the lockup's own blue bar would have dissolved
into it. The blue now does its work as the rule under the header, the
active queue and the primary button, which is how the bank uses it. The
lockup's 3px white keyline is the bank's own, so it sits correctly on the
panel in dark mode too and no reversed variant is needed.

Palette, grounded rather than invented: the page canvas is #F0F6FB, the
tint hdfcbank.com uses behind content; the good/ tokens sit on the hue of
the bank's #00B947 instead of a generic emerald, darkened to #00713D
because #00B947 is 2.6:1 on white and unreadable as a label. The two brand
hexes were already right and are unchanged.

Also closes three AA failures the palette claimed it did not have —
--color-faint in light, and --color-faint / --color-brand in dark, each
below 4.5:1 on --color-panel3. White on --color-signal (4.31:1) is left
alone knowingly: darkening HDFC red for the Decline button would have made
it a different red from the logo beside it.
This commit is contained in:
Yashas 2026-08-17 16:55:50 +05:30
parent 8949a1b0d7
commit 602b570064
12 changed files with 316 additions and 119 deletions

View File

@ -90,6 +90,34 @@ whole app. It is built around three claims:
Rules are never violet; the agent's narrative always is. Nobody should have Rules are never violet; the agent's narrative always is. Nobody should have
to be told which is which. to be told which is which.
## Brand
Everything below was read off HDFC Bank's own production assets rather than
recalled or approximated. Check the source before changing any of it.
| What | Value | Where it came from |
|---|---|---|
| Logo | `src/components/HdfcLogo.tsx` | Coordinates lifted verbatim from `s7ap1.scene7.com/is/content/hdfcbankPWS/hdfc-bank-logo` — the SVG the bank serves in its own site header |
| Blue | `#004C8F` | The lockup's bar fill |
| Red | `#ED232A` | The mark's ground |
| Typeface | Open Sans | hdfcbank.com preloads OpenSans Regular / SemiBold / Bold / ExtraBold and sets its whole site in it. Self-hosted through `@fontsource-variable/open-sans`, never a CDN |
| Page tint | `#F0F6FB` | The tint behind content on hdfcbank.com |
| Success green | hue of `#00B947` | The bank's green, darkened to `#00713D``#00B947` itself is 2.6:1 on white and unreadable as a label |
| Tagline | "We understand your world" | Shown once, on the sign-in card |
**The mark is a RED square with a BLUE centre**, split by a white channel
cross — not a blue square with a red inset. It was drawn inside out here
originally, in the component *and* in the favicon in `index.html`. Those two
carry the same coordinates; change one and you must change the other.
The lockup carries the bank's own 3px white keyline, which is what lets it sit
on a coloured ground — so there is no reversed variant, and none is needed. It
is also why the header is a light surface: HDFC puts the full lockup on a light
ground on every one of its own digital surfaces, and the navy header this app
used to have would have dissolved the lockup's own blue bar into itself. The
bank's blue does its work as the rule under the header, the active queue and
the primary button.
## Theming ## Theming
One attribute — `data-theme` on `<html>` — swaps the whole console. Every colour One attribute — `data-theme` on `<html>` — swaps the whole console. Every colour
@ -97,13 +125,19 @@ is a CSS custom property redefined under `:root[data-theme='dark']` in
`src/styles.css`. There is no second stylesheet and no `dark:` prefix anywhere. `src/styles.css`. There is no second stylesheet and no `dark:` prefix anywhere.
**Never put a literal colour in a component.** A hex or a raw Tailwind shade is **Never put a literal colour in a component.** A hex or a raw Tailwind shade is
invisible to the toggle and will be wrong in one of the two themes. invisible to the toggle and will be wrong in one of the two themes. `HdfcLogo.tsx`
is the single exception, and it earns it: a logo's colours are not design
decisions, so the mark stays `#ED232A` and `#004C8F` in every theme.
The palette is the bank's two colours, and **red is never decoration** — it **Red is never decoration** — it marks the logo and a decline, nothing else. So
marks the logo block and a decline, nothing else. So when red appears in a when red appears in a queue it means something. The referred queue's amber is
queue it means something. The referred queue's amber is the only amber in the the only amber in the pipeline, for the same reason: it should read as "needs
pipeline, for the same reason: it should read as "needs you" without reading as you" without reading as an error. A referred file is not a bad file.
an error. A referred file is not a bad file.
Every text colour clears WCAG AA on each surface it actually lands on, in both
themes. There is one knowing exception: white on `--color-signal` is 4.31:1 —
the Decline button. Darkening HDFC red to clear AA would have made the one red
button on the screen a different red from the logo beside it.
## Known gaps ## Known gaps

View File

@ -5,17 +5,24 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<!--BASE_HREF--> <!--BASE_HREF-->
<title>HDFC Bank | Loan Desk</title> <title>HDFC Bank | Loan Desk</title>
<!-- The logo block as a data-URI favicon: an inline SVG needs no asset, <!-- The bank's mark as a data-URI favicon: an inline SVG needs no asset,
which matters because the build is promoted between environments which matters because the build is promoted between environments
unchanged and a /favicon.ico would resolve against whatever mount path unchanged and a /favicon.ico would resolve against whatever mount path
the server chose. --> the server chose.
These coordinates are the same ones in src/components/HdfcLogo.tsx,
taken off HDFC's own vector: a RED ground, a white channel cross, a
BLUE centre. It was drawn inside out here too — blue outside, red in —
so if you change one of the two, change the other. -->
<link <link
rel="icon" rel="icon"
href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'%3E%3Crect width='32' height='32' fill='%23004C8F'/%3E%3Crect x='6' y='6' width='20' height='20' fill='%23fff'/%3E%3Crect x='9' y='9' width='14' height='14' fill='%23ED232A'/%3E%3C/svg%3E" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 40 40'%3E%3Crect width='40' height='40' fill='%23ED232A'/%3E%3Crect x='7' y='7' width='26' height='26' fill='%23fff'/%3E%3Crect x='18' width='4' height='40' fill='%23fff'/%3E%3Crect y='18' width='40' height='4' fill='%23fff'/%3E%3Crect x='14' y='14' width='12' height='12' fill='%23004C8F'/%3E%3C/svg%3E"
/> />
<!-- Navy chrome on mobile browsers, matching the app header. Kept in step <!-- Mobile browser chrome, matching the app header — which is the panel
with the active theme by applyTheme() in src/theme.ts. --> colour, not the bank's blue: HDFC sets its logo on a light ground
<meta name="theme-color" content="#004c8f" /> everywhere, so the console does too. Kept in step with the active
theme by applyTheme() in src/theme.ts. -->
<meta name="theme-color" content="#ffffff" />
<!-- Theme, resolved BEFORE first paint. <!-- Theme, resolved BEFORE first paint.
This duplicates a few lines of src/theme.ts on purpose. The bundle is a This duplicates a few lines of src/theme.ts on purpose. The bundle is a
module and therefore deferred, so resolving the theme in React means module and therefore deferred, so resolving the theme in React means
@ -35,7 +42,7 @@
document.documentElement.setAttribute('data-theme', t); document.documentElement.setAttribute('data-theme', t);
if (t === 'dark') { if (t === 'dark') {
var m = document.querySelector('meta[name="theme-color"]'); var m = document.querySelector('meta[name="theme-color"]');
if (m) m.setAttribute('content', '#00152b'); if (m) m.setAttribute('content', '#052b52');
} }
} catch (e) { } catch (e) {
document.documentElement.setAttribute('data-theme', 'light'); document.documentElement.setAttribute('data-theme', 'light');

10
package-lock.json generated
View File

@ -8,6 +8,7 @@
"name": "hdfc-loan-desk", "name": "hdfc-loan-desk",
"version": "0.1.0", "version": "0.1.0",
"dependencies": { "dependencies": {
"@fontsource-variable/open-sans": "^5.3.0",
"lucide-react": "^0.454.0", "lucide-react": "^0.454.0",
"react": "^18.3.1", "react": "^18.3.1",
"react-dom": "^18.3.1", "react-dom": "^18.3.1",
@ -696,6 +697,15 @@
"node": ">=12" "node": ">=12"
} }
}, },
"node_modules/@fontsource-variable/open-sans": {
"version": "5.3.0",
"resolved": "https://registry.npmjs.org/@fontsource-variable/open-sans/-/open-sans-5.3.0.tgz",
"integrity": "sha512-VOfhZfLIOBVa6K/dTEu7scqQXYdGa4jBX8quFpQ0S6574B+W2EOBJf9PkqyVtMEI7WWIWNEbpvPc3vY1mN2mmQ==",
"license": "OFL-1.1",
"funding": {
"url": "https://github.com/sponsors/ayuhito"
}
},
"node_modules/@jridgewell/gen-mapping": { "node_modules/@jridgewell/gen-mapping": {
"version": "0.3.13", "version": "0.3.13",
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",

View File

@ -11,6 +11,7 @@
"typecheck": "tsc -b --noEmit" "typecheck": "tsc -b --noEmit"
}, },
"dependencies": { "dependencies": {
"@fontsource-variable/open-sans": "^5.3.0",
"lucide-react": "^0.454.0", "lucide-react": "^0.454.0",
"react": "^18.3.1", "react": "^18.3.1",
"react-dom": "^18.3.1", "react-dom": "^18.3.1",

143
src/components/HdfcLogo.tsx Normal file
View File

@ -0,0 +1,143 @@
/**
* HDFC Bank's logo, reproduced exactly.
*
* WHERE THESE NUMBERS COME FROM
*
* Every coordinate below is lifted from the bank's own vector asset the file
* served in the header of hdfcbank.com:
*
* https://s7ap1.scene7.com/is/content/hdfcbankPWS/hdfc-bank-logo
*
* Nothing here is drawn by eye. That matters because the mark had been drawn by
* eye before, and it was inside out: a blue square with a red one inset. The
* real mark is the other way round a RED ground, a white channel cross that
* splits it into four corner blocks, and a small BLUE square at the centre.
* Getting that backwards is the kind of thing a client notices in the first
* five seconds of a demo.
*
* The mark, normalised to a 40x40 grid, is:
*
* red 0,0 40x40 the ground
* white 7,7 26x26 the inner square
* white 18,0 4x40 the vertical channel
* white 0,18 40x4 the horizontal channel
* blue 14,14 12x12 the centre
*
* THE TWO COLOURS ARE FIXED AND ARE NOT THEME TOKENS. A logo does not restyle
* itself for dark mode: HDFC red is #ED232A and HDFC blue is #004C8F on every
* surface, in every theme, forever. The white is real white for the same
* reason. This is the one file in the app allowed to hold literal colours, and
* the reason it is allowed is that these are not design decisions.
*
* The full lockup carries a 3px white keyline, exactly as the bank's asset
* does. That keyline is what lets the logo sit on a coloured ground without
* the blue bar bleeding into it so the lockup is safe on the panel in either
* theme, and no reversed variant is needed.
*/
/** HDFC red. Fixed. */
const RED = '#ED232A';
/** HDFC blue. Fixed. */
const BLUE = '#004C8F';
/**
* "HDFC BANK", as outlines rather than as text.
*
* The wordmark is Helvetica Black, which is licensed and which we therefore
* cannot ship. Setting it in a substitute would produce a logo that is *nearly*
* right, which is worse than one that is obviously a placeholder. The bank's
* own outlines are exact and cost 3.6 KB, so the path is inlined verbatim from
* the asset above. Do not reformat it.
*/
const WORDMARK =
'M40.7812 24.1165V10.5516H45.2432V15.13H49.4243V10.5516H53.8929V24.1165H49.4243V18.8534H45.2432V24.1165H40.7812ZM60.9539 13.9222H62.1106C62.9432 13.9222 63.5524 14.0085 63.9418 14.1816C64.2299 14.3074 64.4641 14.4955 64.6696 14.7765C64.8571 15.0539 65.0086 15.4038 65.12 15.8435C65.2213 16.2797 65.2752 16.741 65.2752 17.2274C65.2752 18.0207 65.1673 18.6839 64.9325 19.2213C64.7055 19.7581 64.3779 20.1439 63.9562 20.3817C63.5345 20.6238 62.9073 20.7388 62.0783 20.7388L60.9539 20.746V13.9222ZM62.6767 24.1165C63.7291 24.1165 64.6013 24.0303 65.3147 23.8391C66.0323 23.6552 66.6157 23.4102 67.0626 23.1178C67.5172 22.8183 67.9276 22.4151 68.3061 21.9136C68.6811 21.3948 69.0094 20.7424 69.2645 19.9426C69.5281 19.1458 69.6539 18.2477 69.6539 17.2316C69.6539 15.7393 69.3694 14.4776 68.7931 13.4249C68.2091 12.3939 67.416 11.6474 66.4031 11.2083C65.3944 10.7679 64.1904 10.5516 62.7917 10.5516H56.589V24.1165H62.6767ZM71.6983 24.1165V10.5516H82.2797V13.897H76.1022V16.0706H81.044V19.3255H76.1022V24.1165H71.6983ZM97.2225 15.7393H93.0097C92.93 15.0647 92.685 14.5315 92.2777 14.1493C91.8668 13.7742 91.3517 13.5903 90.7173 13.5903C89.9278 13.5903 89.2863 13.897 88.7927 14.5027C88.2955 15.1156 88.0463 16.1065 88.0463 17.4695C88.0463 18.4209 88.1584 19.135 88.3638 19.6215C88.5765 20.1223 88.8754 20.483 89.2467 20.7244C89.6289 20.9659 90.0937 21.0779 90.6664 21.0779C91.3516 21.0779 91.8991 20.9041 92.2993 20.5081C92.7102 20.1367 92.948 19.5819 93.0277 18.839H97.3165C97.2225 19.6826 97.053 20.3889 96.808 20.9659C96.5744 21.5392 96.1557 22.1089 95.5902 22.6817C95.0096 23.2669 94.3119 23.7229 93.5428 24.0195C92.7533 24.3328 91.8416 24.4808 90.7964 24.4808C89.7661 24.4808 88.8035 24.3328 87.9349 24.0303C87.0556 23.7199 86.3128 23.2802 85.7216 22.7284C85.1196 22.1557 84.6512 21.514 84.3163 20.7891C83.8329 19.7694 83.6059 18.6479 83.6059 17.397C83.6059 16.3516 83.7754 15.36 84.1181 14.4524C84.4601 13.5508 84.9327 12.7833 85.5341 12.1632C86.1397 11.5462 86.7992 11.0848 87.5348 10.7751C88.4644 10.3785 89.4881 10.188 90.6233 10.188C91.611 10.188 92.5263 10.3318 93.3913 10.6235C94.2526 10.9159 94.9595 11.3413 95.5105 11.9074C96.0653 12.4662 96.5028 13.1302 96.7972 13.8605C97.0134 14.3763 97.1506 15 97.2225 15.7393M111.514 13.6053C112.144 13.6053 112.552 13.634 112.753 13.6915C113.002 13.7623 113.222 13.9084 113.384 14.1097C113.542 14.3116 113.626 14.5459 113.626 14.8167C113.626 15.1623 113.488 15.4469 113.226 15.6632C112.944 15.8908 112.465 15.9951 111.776 15.9951H110.097V13.6047L111.514 13.6053ZM112.634 24.1165C113.69 24.1165 114.433 24.0662 114.869 23.9728C115.324 23.8615 115.768 23.7043 116.192 23.5037C116.631 23.273 116.97 23.0567 117.193 22.8189C117.54 22.4978 117.795 22.1089 117.983 21.6398C118.17 21.1678 118.264 20.6345 118.264 20.0612C118.264 19.2428 118.062 18.5688 117.652 18.0207C117.241 17.4695 116.689 17.1052 115.994 16.9069C117.172 16.164 117.759 15.2055 117.759 14.0372C117.759 12.83 117.241 11.9074 116.213 11.2802C115.42 10.7967 114.184 10.5516 112.508 10.5516H105.779V24.1165H112.634ZM111.917 18.4352C112.8 18.4352 113.373 18.5431 113.662 18.7707C113.928 18.9906 114.073 19.2932 114.073 19.7005C114.073 20.1115 113.928 20.4362 113.636 20.6633C113.358 20.9047 112.779 21.0054 111.917 21.0054H110.097V18.4352H111.917ZM128.064 19.5568H125.227L126.626 14.6214L128.064 19.5568ZM123.894 24.1165L124.409 22.336H128.849L129.369 24.1165H133.806L128.94 10.5516H124.409L119.54 24.1165H123.894ZM135.679 24.1165V10.5516H139.835L144.524 17.8152V10.5516H148.799V24.1165H144.553L139.964 17.0333V24.1165H135.679ZM151.772 24.1165V10.5516H156.094V15.0251L160.127 10.5516H165.367L160.397 15.6596L165.883 24.1165H160.542L157.279 18.7126L156.093 19.8845V24.1165H151.772Z';
/**
* The mark on its own the red square. Use where the full lockup will not fit:
* a favicon-sized slot, a dense table, a collapsed mobile header.
*/
export function HdfcMark({ size = 32, className = '' }: { size?: number; className?: string }) {
return (
<svg
width={size}
height={size}
viewBox="0 0 40 40"
className={`shrink-0 ${className}`}
role="presentation"
aria-hidden="true"
>
<rect width="40" height="40" fill={RED} />
<rect x="7" y="7" width="26" height="26" fill="#ffffff" />
<rect x="18" width="4" height="40" fill="#ffffff" />
<rect y="18" width="40" height="4" fill="#ffffff" />
<rect x="14" y="14" width="12" height="12" fill={BLUE} />
</svg>
);
}
/**
* The full lockup: mark, blue bar, white wordmark, white keyline.
*
* Sized by HEIGHT only the width follows from the asset's 178:35 ratio, so
* the proportions can never drift. A logo stretched to fit a container is a
* logo used wrongly.
*/
export function HdfcLogo({ height = 30, className = '' }: { height?: number; className?: string }) {
return (
<svg
height={height}
width={(height * 178) / 35}
viewBox="0 0 178 35"
className={`shrink-0 ${className}`}
role="img"
aria-label="HDFC Bank"
>
<title>HDFC Bank</title>
{/* The bar, with the keyline that lets it sit on any ground. */}
<path
d="M3 1.5H1.5V3V31.6689V33.1689H3H175H176.5V31.6689V3V1.5H175H3Z"
fill={BLUE}
stroke="#ffffff"
strokeWidth="3"
/>
<path d="M31.668 3H175V31.6689H31.668V3Z" fill={BLUE} />
<path d={WORDMARK} fill="#ffffff" />
{/* The mark. */}
<path d="M3 3H31.6642V31.6689H3V3Z" fill={RED} />
<path d="M8.01562 8.01758H26.6495V26.6545H8.01562V8.01758Z" fill="#ffffff" />
<path d="M15.9023 3H18.768V31.6689H15.9023V3Z" fill="#ffffff" />
<path d="M3 15.9048H31.6642V18.7709H3V15.9048Z" fill="#ffffff" />
<path d="M13.0312 13.0352H21.6306V21.6365H13.0312V13.0352Z" fill={BLUE} />
</svg>
);
}
/**
* The header identity: the lockup, then the desk's name beside it.
*
* The sub-name is deliberately OUTSIDE the logo. "Loan Desk" is not part of
* HDFC's mark and must never be set as though it were — a bank's identity is
* not a slot you type your product into. A vertical rule and a distinctly
* lighter weight say "this application, at this bank", which is the true
* relationship.
*
* Narrow screens drop to the mark alone rather than shrinking the lockup: at
* phone widths the wordmark's counters close up and it stops reading as HDFC's
* logo at all, where the mark stays recognisable down to favicon size.
*/
export function HdfcAppLogo({ subtitle = 'Loan Desk' }: { subtitle?: string }) {
return (
<div className="flex min-w-0 items-center gap-3">
<span className="sm:hidden">
<HdfcMark size={28} />
</span>
<span className="hidden sm:inline-flex">
<HdfcLogo height={28} />
</span>
<span aria-hidden="true" className="hidden h-7 w-px bg-line lg:block" />
<span className="hidden text-[13px] font-semibold tracking-[0.14em] text-muted uppercase lg:inline">
{subtitle}
</span>
</div>
);
}

View File

@ -1,52 +0,0 @@
/**
* The bank's mark: a blue block with a red square inset, set beside the
* wordmark.
*
* This is a demo asset, drawn from tokens rather than shipped as an image
* file the build is promoted between environments unchanged, so an
* `/logo.png` would resolve against whatever mount path the server chose.
* Drawing it also means it obeys the theme: on the navy header the white
* separator band has to be white, and on a light panel it has to be the panel
* colour, which an image cannot do.
*/
export function HdfcMark({ size = 26, onNavy = false }: { size?: number; onNavy?: boolean }) {
// The inner band reads as the ground showing through. On navy chrome that is
// white; on a light surface it is the panel.
const band = onNavy ? '#ffffff' : 'var(--color-panel)';
return (
<svg
width={size}
height={size}
viewBox="0 0 32 32"
aria-hidden="true"
className="shrink-0"
role="presentation"
>
<rect width="32" height="32" rx="2" fill="var(--color-navy)" />
<rect x="5" y="5" width="22" height="22" fill={band} />
<rect x="8.5" y="8.5" width="15" height="15" fill="var(--color-signal)" />
</svg>
);
}
export function HdfcWordmark({ onNavy = false }: { onNavy?: boolean }) {
return (
<div className="flex items-center gap-2.5">
<HdfcMark onNavy={onNavy} />
<div className="leading-none">
<div
className={`text-[15px] font-bold tracking-tight ${onNavy ? 'text-white' : 'text-ink'}`}
>
HDFC BANK
</div>
<div
className={`mt-1 text-[10px] font-semibold tracking-[0.14em] uppercase ${
onNavy ? 'text-white/70' : 'text-faint'
}`}
>
Loan Desk
</div>
</div>
</div>
);
}

View File

@ -1,7 +1,12 @@
import { Moon, Sun } from 'lucide-react'; import { Moon, Sun } from 'lucide-react';
import { useTheme } from '../theme'; import { useTheme } from '../theme';
export function ThemeToggle({ onNavy = false }: { onNavy?: boolean }) { /**
* The `onNavy` variant went with the navy header. The chrome is `bg-panel` in
* both themes now white in light, navy in dark so one set of token classes
* covers every place this button appears.
*/
export function ThemeToggle() {
const { theme, toggle } = useTheme(); const { theme, toggle } = useTheme();
const next = theme === 'dark' ? 'light' : 'dark'; const next = theme === 'dark' ? 'light' : 'dark';
return ( return (
@ -9,11 +14,7 @@ export function ThemeToggle({ onNavy = false }: { onNavy?: boolean }) {
onClick={toggle} onClick={toggle}
title={`Switch to ${next} mode`} title={`Switch to ${next} mode`}
aria-label={`Switch to ${next} mode`} aria-label={`Switch to ${next} mode`}
className={`rounded-md p-2 transition-colors focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 ${ className="rounded-md p-2 text-muted transition-colors hover:bg-panel2 hover:text-ink focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand"
onNavy
? 'text-white/75 hover:bg-white/10 hover:text-white focus-visible:outline-white'
: 'text-muted hover:bg-panel2 hover:text-ink focus-visible:outline-brand'
}`}
> >
{theme === 'dark' ? <Sun className="h-4 w-4" /> : <Moon className="h-4 w-4" />} {theme === 'dark' ? <Sun className="h-4 w-4" /> : <Moon className="h-4 w-4" />}
</button> </button>

View File

@ -2,13 +2,24 @@ import { NavLink, Outlet, useNavigate } from 'react-router-dom';
import { LogOut, Plus } from 'lucide-react'; import { LogOut, Plus } from 'lucide-react';
import { useZino } from '../api/provider'; import { useZino } from '../api/provider';
import { QUEUES, prettyRole } from '../api/config'; import { QUEUES, prettyRole } from '../api/config';
import { HdfcWordmark } from '../components/HdfcMark'; import { HdfcAppLogo } from '../components/HdfcLogo';
import { ThemeToggle } from '../components/ThemeToggle'; import { ThemeToggle } from '../components/ThemeToggle';
import { Button } from '../components/core'; import { Button } from '../components/core';
/** /**
* The chrome. * The chrome.
* *
* WHY THE HEADER IS A LIGHT SURFACE AND NOT A NAVY BAR
*
* It used to be a navy band with the wordmark set in white on top of it. That
* is not how HDFC Bank presents itself anywhere: every one of the bank's own
* digital surfaces puts the FULL LOCKUP red mark, blue bar, white "HDFC
* BANK" on a light ground. The navy bar also made the real logo impossible
* to use, because the lockup's own blue bar would have dissolved into it. So
* the chrome gives the logo the ground it was drawn for, and the bank's blue
* does its work where it belongs: the rule under the header, the active queue,
* and the primary button.
*
* The sidebar IS the pipeline: every entry is the record view filtered on * The sidebar IS the pipeline: every entry is the record view filtered on
* current_state_name, in the order a file actually moves. That is the whole * current_state_name, in the order a file actually moves. That is the whole
* navigation model, and it means a person learns the process by using the app. * navigation model, and it means a person learns the process by using the app.
@ -26,44 +37,42 @@ export function Shell() {
return ( return (
<div className="flex min-h-full flex-col"> <div className="flex min-h-full flex-col">
{/* Header — navy in both themes. The bank's colour is not a surface. */} <header className="sticky top-0 z-30 bg-panel">
<header className="sticky top-0 z-30 border-b border-deepnavy bg-navy">
<div className="flex items-center gap-4 px-4 py-2.5"> <div className="flex items-center gap-4 px-4 py-2.5">
<HdfcWordmark onNavy /> <HdfcAppLogo />
<div className="ml-auto flex items-center gap-3"> <div className="ml-auto flex items-center gap-3">
<Button <Button kind="primary" onClick={() => navigate('/new')}>
kind="secondary"
onClick={() => navigate('/new')}
className="!bg-white/10 !text-white !ring-white/20 hover:!bg-white/20"
>
<Plus className="h-4 w-4" /> <Plus className="h-4 w-4" />
New Application New Application
</Button> </Button>
{user && ( {user && (
<div className="hidden text-right sm:block"> <div className="hidden text-right sm:block">
<div className="text-[13px] leading-tight font-semibold text-white"> <div className="text-[13px] leading-tight font-semibold text-ink">
{user.name || user.email} {user.name || user.email}
</div> </div>
<div className="text-[11px] leading-tight text-white/65"> <div className="text-[11px] leading-tight text-faint">
{(user.roles ?? []).map(prettyRole).join(' · ') || 'No role'} {(user.roles ?? []).map(prettyRole).join(' · ') || 'No role'}
</div> </div>
</div> </div>
)} )}
<ThemeToggle onNavy /> <ThemeToggle />
<button <button
onClick={logout} onClick={logout}
title="Sign out" title="Sign out"
aria-label="Sign out" aria-label="Sign out"
className="rounded-md p-2 text-white/75 transition-colors hover:bg-white/10 hover:text-white focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-white" className="rounded-md p-2 text-muted transition-colors hover:bg-panel2 hover:text-ink focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand"
> >
<LogOut className="h-4 w-4" /> <LogOut className="h-4 w-4" />
</button> </button>
</div> </div>
</div> </div>
{/* The bank's blue, drawn as a rule rather than a field. A 3px band is
enough to brand the chrome; a 56px one competes with the logo. */}
<div aria-hidden="true" className="h-[3px] bg-navy" />
</header> </header>
<div className="flex flex-1 flex-col lg:flex-row"> <div className="flex flex-1 flex-col lg:flex-row">

View File

@ -4,6 +4,13 @@ import { BrowserRouter } from 'react-router-dom';
import { App } from './App'; import { App } from './App';
import { ZinoProvider } from './api/provider'; import { ZinoProvider } from './api/provider';
import { basePath, requireConfigValue } from './runtimeConfig'; import { basePath, requireConfigValue } from './runtimeConfig';
// Open Sans is HDFC Bank's typeface — their site self-hosts it and sets both
// body and headings in it. Self-hosted here for the same reason the API URL is
// read at runtime: this build is promoted between environments and dropped
// behind bank proxies, and a font fetched from a CDN is a font that sometimes
// does not arrive. `wght` is the weight-axis-only variable file, which covers
// the 400 / 600 / 700 / 800 this design uses in a single download.
import '@fontsource-variable/open-sans/wght.css';
import './styles.css'; import './styles.css';
/** /**

View File

@ -2,7 +2,7 @@ import { useState } from 'react';
import { useZino } from '../api/provider'; import { useZino } from '../api/provider';
import { ORG_ID } from '../api/config'; import { ORG_ID } from '../api/config';
import { Button, ErrorNote, Input, Label } from '../components/core'; import { Button, ErrorNote, Input, Label } from '../components/core';
import { HdfcWordmark } from '../components/HdfcMark'; import { HdfcAppLogo, HdfcLogo } from '../components/HdfcLogo';
import { ThemeToggle } from '../components/ThemeToggle'; import { ThemeToggle } from '../components/ThemeToggle';
/** /**
@ -38,9 +38,12 @@ export function Login() {
return ( return (
<div className="flex min-h-full flex-col bg-bg"> <div className="flex min-h-full flex-col bg-bg">
<header className="flex items-center justify-between border-b border-deepnavy bg-navy px-4 py-2.5"> <header className="bg-panel">
<HdfcWordmark onNavy /> <div className="flex items-center justify-between px-4 py-2.5">
<ThemeToggle onNavy /> <HdfcAppLogo />
<ThemeToggle />
</div>
<div aria-hidden="true" className="h-[3px] bg-navy" />
</header> </header>
<div className="flex flex-1 items-center justify-center p-4"> <div className="flex flex-1 items-center justify-center p-4">
@ -51,16 +54,20 @@ export function Login() {
}} }}
className="lift w-full max-w-sm rounded-lg border border-line bg-panel p-6" className="lift w-full max-w-sm rounded-lg border border-line bg-panel p-6"
> >
<h1 className="text-lg font-semibold text-ink">Sign in</h1> {/* The lockup again, above the fold of the only screen a signed-out
person sees. A sign-in page is where someone checks they are at
the right bank before typing a password into it. */}
<HdfcLogo height={30} />
<h1 className="mt-5 text-lg font-semibold text-ink">Sign in</h1>
<p className="mt-1 text-sm text-muted"> <p className="mt-1 text-sm text-muted">
MSME and personal loan origination. Loan Desk MSME and personal loan origination.
</p> </p>
<div className="mt-5 space-y-3"> <div className="mt-5 space-y-3">
{err && <ErrorNote>{err}</ErrorNote>} {err && <ErrorNote>{err}</ErrorNote>}
<div> <div>
<Label required>Email</Label> <Label required>Email</Label>
<Input value={email} onChange={setEmail} type="email" placeholder="name@hdfc.example" /> <Input value={email} onChange={setEmail} type="email" placeholder="name@hdfcbank.com" />
</div> </div>
<div> <div>
<Label required>Password</Label> <Label required>Password</Label>
@ -70,6 +77,13 @@ export function Login() {
Sign in Sign in
</Button> </Button>
</div> </div>
{/* The bank's line, set the way the bank sets it under the mark,
light, small, and not shouted. It belongs on the one screen that
is purely the bank talking to its own staff, and nowhere else. */}
<p className="mt-6 border-t border-line pt-4 text-center text-[11px] tracking-wide text-faint">
We understand your world
</p>
</form> </form>
</div> </div>
</div> </div>

View File

@ -17,11 +17,25 @@
className is invisible to the toggle and will be wrong in one of the two className is invisible to the toggle and will be wrong in one of the two
themes. That is what the semantic tone tokens below are for. themes. That is what the semantic tone tokens below are for.
THE PALETTE IS HDFC'S TWO COLOURS. Their identity is a navy blue and a WHERE THE BRAND VALUES COME FROM
red, and the discipline here is that RED IS NEVER DECORATION. It marks
the logo block, a decline, and nothing else so when red appears in the Not from memory and not from a palette site. The two brand colours, the
queue it means something. The pipeline's own accent work is done by the page tint and the typeface were all read off HDFC Bank's own production
blue and by the amber that marks the referred queue. assets:
#004C8F, #ED232A the logo's two fills, taken from the vector the bank
serves in its own site header (see HdfcLogo.tsx)
#F0F6FB the page tint used behind content on hdfcbank.com
Open Sans the bank self-hosts OpenSans Regular / SemiBold /
Bold / ExtraBold and sets its whole site in it
#00B947 the bank's success green used here only as the HUE
the good/ tokens sit on, because #00B947 itself is
2.6:1 on white and unreadable as text
THE DISCIPLINE: RED IS NEVER DECORATION. It marks the logo and a decline,
and nothing else so when red appears in the queue it means something.
The pipeline's accent work is done by the blue and by the amber that marks
the referred queue.
Every colour used for text is checked at WCAG AA against the surfaces it Every colour used for text is checked at WCAG AA against the surfaces it
actually appears on, in both themes. */ actually appears on, in both themes. */
@ -29,12 +43,12 @@
/* --- Structural ---------------------------------------------------- */ /* --- Structural ---------------------------------------------------- */
--color-ink: #10243d; /* primary text */ --color-ink: #10243d; /* primary text */
--color-muted: #4a5f7a; /* secondary text */ --color-muted: #4a5f7a; /* secondary text */
--color-faint: #6b7f96; /* labels, placeholders, dashes */ --color-faint: #586c83; /* labels, placeholders, dashes */
--color-bg: #f6f8fb; /* page canvas */ --color-bg: #f0f6fb; /* page canvas — hdfcbank.com's */
--color-panel: #ffffff; /* card face */ --color-panel: #ffffff; /* card face */
--color-panel2: #f1f5fa; /* input fill, row hover */ --color-panel2: #f1f6fb; /* input fill, row hover */
--color-panel3: #e4ecf5; /* pressed / hover on panel2 */ --color-panel3: #e2ecf7; /* pressed / hover on panel2 */
--color-line: #d8e2ee; /* borders */ --color-line: #d6e3f0; /* borders */
/* Brand accent. Used BOTH as a fill and as text, so it carries a /* Brand accent. Used BOTH as a fill and as text, so it carries a
companion for whatever sits on top of it. */ companion for whatever sits on top of it. */
@ -43,10 +57,9 @@
--color-brand-hover: #003a6f; --color-brand-hover: #003a6f;
/* --- HDFC palette, fixed in both themes ---------------------------- */ /* --- HDFC palette, fixed in both themes ---------------------------- */
/* Surfaces invert between themes; the bank's own colours do not. The /* Surfaces invert between themes; the bank's own colours do not. These two
header is navy in dark mode too. */ are the exact fills of the bank's logo and are never redefined below. */
--color-navy: #004c8f; --color-navy: #004c8f;
--color-deepnavy: #002e5f;
--color-signal: #ed232a; /* HDFC red — declines and the logo block */ --color-signal: #ed232a; /* HDFC red — declines and the logo block */
--color-signal-hover: #c91d23; --color-signal-hover: #c91d23;
@ -75,9 +88,12 @@
--color-info-soft: #e8f1f9; --color-info-soft: #e8f1f9;
--color-info-edge: #a9c9e6; --color-info-edge: #a9c9e6;
--color-good: #0a6b4a; /* Sat on HDFC's own success green (#00B947) rather than a generic emerald,
--color-good-soft: #eafaf3; but darkened until it clears AA: the bank's green is 2.6:1 on white,
--color-good-edge: #94ceb4; which is a fine fill and an unreadable label. Same hue, usable weight. */
--color-good: #00713d;
--color-good-soft: #e4f6ec;
--color-good-edge: #93d3b0;
/* Drawn from HDFC red rather than Tailwind's rose, so a Declined badge /* Drawn from HDFC red rather than Tailwind's rose, so a Declined badge
and a Decline button are visibly the same red rather than two and a Decline button are visibly the same red rather than two
@ -99,12 +115,15 @@
dialogue. */ dialogue. */
--color-scrim: rgb(0 24 48 / 0.45); --color-scrim: rgb(0 24 48 / 0.45);
/* HDFC's own typeface is not licensable for redistribution, so the stack /* Open Sans what HDFC Bank actually sets its digital estate in. Their
leads with what a bank desktop is likely to have and degrades through site preloads OpenSans Regular / SemiBold / Bold / ExtraBold as woff2 and
humanist relatives. */ uses nothing else for body or headings.
Self-hosted through @fontsource-variable/open-sans (imported in
main.tsx), never a CDN: this build is promoted between environments and
a bank desktop behind a proxy that cannot reach fonts.googleapis.com
would silently fall back and render as a different product. */
--font-sans: --font-sans:
'Segoe UI', Inter, 'Source Sans 3', system-ui, -apple-system, 'Helvetica Neue', Arial, 'Open Sans Variable', 'Open Sans', system-ui, -apple-system, 'Segoe UI', Arial, sans-serif;
sans-serif;
} }
/* Non-Tailwind vars: consumed by the hand-rolled rules at the bottom rather /* Non-Tailwind vars: consumed by the hand-rolled rules at the bottom rather
@ -130,7 +149,7 @@
:root[data-theme='dark'] { :root[data-theme='dark'] {
--color-ink: #e8f0f9; --color-ink: #e8f0f9;
--color-muted: #a6c0dc; --color-muted: #a6c0dc;
--color-faint: #8aa6c6; --color-faint: #99b3cf;
--color-bg: #00152b; --color-bg: #00152b;
--color-panel: #052b52; --color-panel: #052b52;
--color-panel2: #0b3762; --color-panel2: #0b3762;
@ -140,9 +159,9 @@
/* The accent lightens and the text on it darkens the inverse of light. /* The accent lightens and the text on it darkens the inverse of light.
A #004C8F fill would vanish into the canvas, and #004C8F text on navy A #004C8F fill would vanish into the canvas, and #004C8F text on navy
is unreadable. */ is unreadable. */
--color-brand: #6fa8e8; --color-brand: #7cb2ec;
--color-brand-fg: #00152b; --color-brand-fg: #00152b;
--color-brand-hover: #8fbdf0; --color-brand-hover: #93c2f2;
/* HDFC red is unchanged: white on it clears AA either way, and it still /* HDFC red is unchanged: white on it clears AA either way, and it still
clears 3:1 against the navy canvas as a large component. */ clears 3:1 against the navy canvas as a large component. */
@ -160,9 +179,9 @@
--color-info-soft: #0f3763; --color-info-soft: #0f3763;
--color-info-edge: #467cb8; --color-info-edge: #467cb8;
--color-good: #7fd6a8; --color-good: #5fd694;
--color-good-soft: #08301f; --color-good-soft: #052a18;
--color-good-edge: #2c6a4a; --color-good-edge: #10683b;
--color-bad: #f4a3a6; --color-bad: #f4a3a6;
--color-bad-soft: #3c1416; --color-bad-soft: #3c1416;

View File

@ -56,9 +56,13 @@ export function resolveTheme(): Theme {
export function applyTheme(theme: Theme): void { export function applyTheme(theme: Theme): void {
document.documentElement.setAttribute('data-theme', theme); document.documentElement.setAttribute('data-theme', theme);
// Keep the mobile browser chrome in step with the header it sits above. // Keep the mobile browser chrome in step with the header it sits above.
// That is --color-panel in both themes, not the bank's blue: the header is a
// light surface so the real logo lockup has the ground it was drawn for.
// Keep these two literals in step with `--color-panel` in styles.css and
// with the inline script in index.html.
document document
.querySelector('meta[name="theme-color"]') .querySelector('meta[name="theme-color"]')
?.setAttribute('content', theme === 'dark' ? '#00152b' : '#004c8f'); ?.setAttribute('content', theme === 'dark' ? '#052b52' : '#ffffff');
} }
export function useTheme(): { theme: Theme; setTheme: (t: Theme) => void; toggle: () => void } { export function useTheme(): { theme: Theme; setTheme: (t: Theme) => void; toggle: () => void } {