RRS First version

This commit is contained in:
2026-09-12 03:08:23 -04:00
commit c050d25a84
41 changed files with 5575 additions and 0 deletions
+5
View File
@@ -0,0 +1,5 @@
node_modules/
dist/
test-results/
playwright-report/
.DS_Store
+18
View File
@@ -0,0 +1,18 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="theme-color" content="#f5f3ee" />
<meta
name="description"
content="Radhe Radhe Synthetics — greige knitted fabrics engineered for diverse apparel applications. Consistency at scale. Continuous development."
/>
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<title>RRS — Radhe Radhe Synthetics</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.jsx"></script>
</body>
</html>
+1943
View File
File diff suppressed because it is too large Load Diff
+22
View File
@@ -0,0 +1,22 @@
{
"name": "rrs",
"private": true,
"version": "1.0.0",
"type": "module",
"scripts": {
"dev": "vite --host 0.0.0.0",
"build": "vite build",
"preview": "vite preview --host 0.0.0.0",
"test": "playwright test"
},
"dependencies": {
"react": "^19.1.0",
"react-dom": "^19.1.0",
"react-router-dom": "^7.6.0"
},
"devDependencies": {
"@playwright/test": "^1.52.0",
"@vitejs/plugin-react": "^4.5.0",
"vite": "^6.3.5"
}
}
+14
View File
@@ -0,0 +1,14 @@
import { defineConfig } from "@playwright/test";
export default defineConfig({
testDir: "./tests",
fullyParallel: true,
workers: 2,
reporter: "list",
use: { baseURL: "http://127.0.0.1:5173", screenshot: "only-on-failure" },
webServer: {
command: "npm run dev -- --port 5173 --strictPort",
url: "http://127.0.0.1:5173",
reuseExistingServer: !process.env.CI,
},
});
+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64"><rect width="64" height="64" rx="14" fill="#282a23"/><path d="M18 47V17h14c15 0 15 20 0 20H18m14 0 13 10" fill="none" stroke="#f5f3ee" stroke-width="5"/></svg>

After

Width:  |  Height:  |  Size: 220 B

+93
View File
@@ -0,0 +1,93 @@
Copyright 2014 The DM Sans Project Authors (https://github.com/googlefonts/dm-fonts)
This Font Software is licensed under the SIL Open Font License, Version 1.1.
This license is copied below, and is also available with a FAQ at:
https://scripts.sil.org/OFL
-----------------------------------------------------------
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
-----------------------------------------------------------
PREAMBLE
The goals of the Open Font License (OFL) are to stimulate worldwide
development of collaborative font projects, to support the font creation
efforts of academic and linguistic communities, and to provide a free and
open framework in which fonts may be shared and improved in partnership
with others.
The OFL allows the licensed fonts to be used, studied, modified and
redistributed freely as long as they are not sold by themselves. The
fonts, including any derivative works, can be bundled, embedded,
redistributed and/or sold with any software provided that any reserved
names are not used by derivative works. The fonts and derivatives,
however, cannot be released under any other type of license. The
requirement for fonts to remain under this license does not apply
to any document created using the fonts or their derivatives.
DEFINITIONS
"Font Software" refers to the set of files released by the Copyright
Holder(s) under this license and clearly marked as such. This may
include source files, build scripts and documentation.
"Reserved Font Name" refers to any names specified as such after the
copyright statement(s).
"Original Version" refers to the collection of Font Software components as
distributed by the Copyright Holder(s).
"Modified Version" refers to any derivative made by adding to, deleting,
or substituting -- in part or in whole -- any of the components of the
Original Version, by changing formats or by porting the Font Software to a
new environment.
"Author" refers to any designer, engineer, programmer, technical
writer or other person who contributed to the Font Software.
PERMISSION & CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining
a copy of the Font Software, to use, study, copy, merge, embed, modify,
redistribute, and sell modified and unmodified copies of the Font
Software, subject to the following conditions:
1) Neither the Font Software nor any of its individual components,
in Original or Modified Versions, may be sold by itself.
2) Original or Modified Versions of the Font Software may be bundled,
redistributed and/or sold with any software, provided that each copy
contains the above copyright notice and this license. These can be
included either as stand-alone text files, human-readable headers or
in the appropriate machine-readable metadata fields within text or
binary files as long as those fields can be easily viewed by the user.
3) No Modified Version of the Font Software may use the Reserved Font
Name(s) unless explicit written permission is granted by the corresponding
Copyright Holder. This restriction only applies to the primary font name as
presented to the users.
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
Software shall not be used to promote, endorse or advertise any
Modified Version, except to acknowledge the contribution(s) of the
Copyright Holder(s) and the Author(s) or with their explicit written
permission.
5) The Font Software, modified or unmodified, in part or in whole,
must be distributed entirely under this license, and must not be
distributed under any other license. The requirement for fonts to
remain under this license does not apply to any document created
using the Font Software.
TERMINATION
This license becomes null and void if any of the above conditions are
not met.
DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
OTHER DEALINGS IN THE FONT SOFTWARE.
Binary file not shown.
+93
View File
@@ -0,0 +1,93 @@
Copyright 2022 The Instrument Serif Project Authors (https://github.com/Instrument/instrument-serif)
This Font Software is licensed under the SIL Open Font License, Version 1.1.
This license is copied below, and is also available with a FAQ at:
https://scripts.sil.org/OFL
-----------------------------------------------------------
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
-----------------------------------------------------------
PREAMBLE
The goals of the Open Font License (OFL) are to stimulate worldwide
development of collaborative font projects, to support the font creation
efforts of academic and linguistic communities, and to provide a free and
open framework in which fonts may be shared and improved in partnership
with others.
The OFL allows the licensed fonts to be used, studied, modified and
redistributed freely as long as they are not sold by themselves. The
fonts, including any derivative works, can be bundled, embedded,
redistributed and/or sold with any software provided that any reserved
names are not used by derivative works. The fonts and derivatives,
however, cannot be released under any other type of license. The
requirement for fonts to remain under this license does not apply
to any document created using the fonts or their derivatives.
DEFINITIONS
"Font Software" refers to the set of files released by the Copyright
Holder(s) under this license and clearly marked as such. This may
include source files, build scripts and documentation.
"Reserved Font Name" refers to any names specified as such after the
copyright statement(s).
"Original Version" refers to the collection of Font Software components as
distributed by the Copyright Holder(s).
"Modified Version" refers to any derivative made by adding to, deleting,
or substituting -- in part or in whole -- any of the components of the
Original Version, by changing formats or by porting the Font Software to a
new environment.
"Author" refers to any designer, engineer, programmer, technical
writer or other person who contributed to the Font Software.
PERMISSION & CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining
a copy of the Font Software, to use, study, copy, merge, embed, modify,
redistribute, and sell modified and unmodified copies of the Font
Software, subject to the following conditions:
1) Neither the Font Software nor any of its individual components,
in Original or Modified Versions, may be sold by itself.
2) Original or Modified Versions of the Font Software may be bundled,
redistributed and/or sold with any software, provided that each copy
contains the above copyright notice and this license. These can be
included either as stand-alone text files, human-readable headers or
in the appropriate machine-readable metadata fields within text or
binary files as long as those fields can be easily viewed by the user.
3) No Modified Version of the Font Software may use the Reserved Font
Name(s) unless explicit written permission is granted by the corresponding
Copyright Holder. This restriction only applies to the primary font name as
presented to the users.
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
Software shall not be used to promote, endorse or advertise any
Modified Version, except to acknowledge the contribution(s) of the
Copyright Holder(s) and the Author(s) or with their explicit written
permission.
5) The Font Software, modified or unmodified, in part or in whole,
must be distributed entirely under this license, and must not be
distributed under any other license. The requirement for fonts to
remain under this license does not apply to any document created
using the Font Software.
TERMINATION
This license becomes null and void if any of the above conditions are
not met.
DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
OTHER DEALINGS IN THE FONT SOFTWARE.
Binary file not shown.
Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 837 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 720 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 499 KiB

+48
View File
@@ -0,0 +1,48 @@
"""Create original illustrative knit textures, not photographs of RRS products.
Requires Pillow and NumPy. Generated assets are committed; runtime needs neither.
"""
from pathlib import Path
import math
import random
import numpy as np
from PIL import Image, ImageDraw, ImageFilter
OUTPUT = Path(__file__).resolve().parents[1] / 'public' / 'images'
OUTPUT.mkdir(parents=True, exist_ok=True)
def texture(name, spacing, base, seed):
rng = random.Random(seed)
width, height = 1600, 1200
image = Image.new('RGB', (width, height), tuple(round(c * .7) for c in base))
draw = ImageDraw.Draw(image)
for row in range(-2, round(height / (spacing * .72)) + 3):
for column in range(-2, width // spacing + 3):
x = column * spacing + math.sin(row * .16) * spacing * .16
y = row * spacing * .72
tone = rng.uniform(.91, 1.05)
for side in (-1, 1):
points = []
for step in range(35):
t = step / 34
px = x + side * spacing * (.04 + .43 * (1 - t) + .07 * math.sin(t * math.pi))
py = y + spacing * (t * .95 - .15)
points.append((px, py))
draw.line([(px + 3, py + 4) for px, py in points], fill=tuple(round(c * .55) for c in base), width=round(spacing * .36))
draw.line(points, fill=tuple(min(255, round(c * tone * .84)) for c in base), width=round(spacing * .32))
for strand in range(11):
offset = (strand - 5) * spacing * .027
fibre = [(px + offset + math.sin(i * .9 + strand) * .7, py) for i, (px, py) in enumerate(points)]
light = tone * (.78 + .24 * math.sin(strand * .28))
draw.line(fibre, fill=tuple(min(255, round(c * light)) for c in base), width=max(1, spacing // 32))
pixels = np.asarray(image).astype(float)
yy, xx = np.mgrid[0:height, 0:width]
folds = .91 + .14 * np.sin(xx / 210 + yy / 380) + .05 * np.sin(xx / 82 - yy / 230)
noise = np.random.default_rng(seed).normal(0, 2.1, (height, width))
pixels = np.clip(pixels * folds[:, :, None] + noise[:, :, None], 0, 255).astype('uint8')
Image.fromarray(pixels).filter(ImageFilter.GaussianBlur(.3)).save(OUTPUT / name, quality=90, optimize=True)
texture('knit-natural.jpg', 70, (218, 205, 181), 21)
texture('knit-fine.jpg', 27, (221, 214, 197), 42)
texture('knit-structured.jpg', 108, (180, 170, 147), 63)
+86
View File
@@ -0,0 +1,86 @@
@font-face {
font-family: "Instrument Serif";
src: url("/fonts/instrument-serif-regular.ttf") format("truetype");
font-weight: 400;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: "Instrument Serif";
src: url("/fonts/instrument-serif-italic.ttf") format("truetype");
font-weight: 400;
font-style: italic;
font-display: swap;
}
@font-face {
font-family: "DM Sans";
src: url("/fonts/dm-sans.woff2") format("woff2");
font-weight: 100 1000;
font-style: normal;
font-display: swap;
}
:root {
--paper: #f5f3ee;
--ink: #282a23;
--muted: #686b61;
--line: #d9d9cf;
--wash: #ecebe4;
--accent: #8f725c;
--serif: "Instrument Serif", Georgia, serif;
--sans: "DM Sans", Arial, sans-serif;
--mono: "Courier New", monospace;
--page-gutter: clamp(20px, 4vw, 72px);
color-scheme: light;
font-family: var(--sans);
color: var(--ink);
background: var(--paper);
font-synthesis: none;
-webkit-font-smoothing: antialiased;
}
/* Only box sizing, base document defaults and accessibility are global.
Every visual component owns a BEM block in its own stylesheet. */
*,
*::before,
*::after {
box-sizing: border-box;
}
body {
margin: 0;
min-width: 320px;
}
button,
input,
textarea,
select {
font: inherit;
}
button,
a {
-webkit-tap-highlight-color: transparent;
}
:focus-visible:not([tabindex="-1"]) {
outline: 2px solid var(--accent);
outline-offset: 5px;
}
::selection {
background: #d1d3bf;
color: #282a23;
}
.not-found {
padding: 100px 0;
}
.not-found__label {
font-size: 14px;
font-weight: 500;
color: var(--muted);
}
.not-found__title {
font-family: var(--serif);
font-size: 72px;
font-weight: 400;
margin: 20px 0;
}
.not-found__text {
margin-bottom: 35px;
color: var(--muted);
}
+63
View File
@@ -0,0 +1,63 @@
.capacity-value {
display: inline-flex;
align-items: baseline;
line-height: 1;
white-space: nowrap;
}
.capacity-value__window {
display: inline-block;
height: 1em;
overflow: hidden;
}
.capacity-value__reel {
display: flex;
flex-direction: column;
height: 4em;
}
.capacity-value__reel--up {
transform: translateY(0);
}
.capacity-value__reel--down {
transform: translateY(-3em);
}
.capacity-value__step {
display: block;
height: 1em;
flex: 0 0 1em;
line-height: 1;
}
.capacity-value__dash {
display: inline-block;
}
.reveal--visible .capacity-value__reel--up {
animation: capacity-roll-up 1.2s cubic-bezier(0.22, 1, 0.36, 1) 180ms both;
}
.reveal--visible .capacity-value__reel--down {
animation: capacity-roll-down 1.2s cubic-bezier(0.22, 1, 0.36, 1) 180ms both;
}
@keyframes capacity-roll-up {
from {
transform: translateY(0);
}
to {
transform: translateY(-3em);
}
}
@keyframes capacity-roll-down {
from {
transform: translateY(-3em);
}
to {
transform: translateY(0);
}
}
@media (prefers-reduced-motion: reduce) {
.reveal--visible .capacity-value__reel--up {
animation: none;
transform: translateY(-3em);
}
.reveal--visible .capacity-value__reel--down {
animation: none;
transform: translateY(0);
}
}
+29
View File
@@ -0,0 +1,29 @@
import "./CapacityValue.css";
function Reel({ values, direction }) {
return (
<span className="capacity-value__window" aria-hidden="true">
<span
className={`capacity-value__reel capacity-value__reel--${direction}`}
>
{values.map((value) => (
<span className="capacity-value__step" key={value}>
{value}
</span>
))}
</span>
</span>
);
}
export default function CapacityValue() {
return (
<span className="capacity-value" aria-label="60 to 70">
<Reel values={["00", "20", "40", "60"]} direction="up" />
<span className="capacity-value__dash" aria-hidden="true">
</span>
<Reel values={["70", "80", "90", "99"]} direction="down" />
</span>
);
}
+125
View File
@@ -0,0 +1,125 @@
.gallery {
padding-top: 100px;
scroll-margin-top: 30px;
}
.gallery__intro {
margin: 0 0 22px;
}
.gallery__grid {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 38px 26px;
}
.sample {
min-width: 0;
}
.sample__visual {
position: relative;
aspect-ratio: 1 / 1.12;
overflow: hidden;
border-radius: 4px;
background: var(--wash);
}
.sample__image {
width: 100%;
height: 100%;
display: block;
object-fit: cover;
transition: transform 700ms ease;
}
.sample:hover .sample__image {
transform: scale(1.045);
}
.sample__badge {
position: absolute;
top: 16px;
left: 16px;
padding: 7px 10px;
background: #f5f3eedb;
border-radius: 2px;
font-size: 11px;
}
.sample__heading {
display: flex;
justify-content: space-between;
align-items: baseline;
gap: 12px;
padding-top: 20px;
}
.sample__name {
font-family: var(--serif);
font-size: 27px;
font-weight: 400;
letter-spacing: -0.035em;
margin: 0;
}
.sample__detail {
font-size: 11px;
color: var(--muted);
text-align: right;
}
.sample__pending {
font-size: 11px;
color: var(--muted);
margin: 9px 0 0;
}
.gallery__notice {
display: flex;
align-items: flex-start;
gap: 14px;
margin-top: 32px;
padding: 19px 22px;
background: var(--wash);
border-radius: 4px;
color: var(--muted);
}
.gallery__notice-mark {
flex-shrink: 0;
width: 17px;
height: 17px;
display: grid;
place-content: center;
border: 1px solid currentColor;
border-radius: 50%;
font-family: var(--serif);
font-size: 12px;
}
.gallery__notice-text {
font-size: 11px;
line-height: 1.8;
margin: 0;
}
@media (max-width: 900px) {
.sample__heading {
align-items: flex-start;
flex-direction: column;
gap: 6px;
}
}
@media (max-width: 600px) {
.gallery {
padding-top: 60px;
}
.gallery__grid {
grid-template-columns: 1fr;
gap: 30px;
}
.sample__visual {
aspect-ratio: 1.15;
}
.sample__heading {
flex-direction: row;
align-items: baseline;
}
.gallery__notice {
padding: 16px;
}
}
@media (prefers-reduced-motion: reduce) {
.sample__image {
transition: none;
}
.sample:hover .sample__image {
transform: none;
}
}
+83
View File
@@ -0,0 +1,83 @@
import { developments } from "../data";
import { Action, SectionHeading } from "./Primitives";
import "./Gallery.css";
const textures = ["fine", "natural", "structured"];
export default function Gallery({ preview = false }) {
const items = preview ? developments.slice(0, 3) : developments;
return (
<section className="gallery" id="developments" tabIndex={-1}>
<SectionHeading
centered
label="Selected developments"
title={
preview ? (
<>
Structure changes
<br />
<em>everything.</em>
</>
) : (
<>
From greige
<br />
<em>to possibility.</em>
</>
)
}
>
<p className="gallery__intro">
{preview
? "From subtle surface structures to more expressive knitted textures, different yarn combinations and machine setups produce entirely different fabric characteristics."
: "Selected developments are processed separately as reference samples to explore how a construction can translate after dyeing and finishing. RRS commercially manufactures greige fabric only."}
</p>
{preview && (
<Action to="/fabrics#developments" variant="outline">
View developments
</Action>
)}
</SectionHeading>
<div className="gallery__grid">
{items.map((name, index) => (
<article className="sample" key={name}>
<div className="sample__visual">
<img
className="sample__image"
src={`/images/knit-${textures[index % textures.length]}.jpg`}
alt={`Illustrative placeholder for ${name}; RRS sample photograph not yet supplied`}
width="1600"
height="1200"
loading="lazy"
/>
<span className="sample__badge">Image placeholder</span>
</div>
<div className="sample__heading">
<h3 className="sample__name">{name}</h3>
<span className="sample__detail">Development reference</span>
</div>
<p className="sample__pending">RRS sample photography to follow.</p>
</article>
))}
</div>
<aside className="gallery__notice">
<span className="gallery__notice-mark" aria-hidden="true">
i
</span>
<div>
<p className="gallery__notice-text">
Illustrative textures shown, not actual RRS samples. Development
names are references, not technical categories.
</p>
<p className="gallery__notice-text">
When supplied, processed images will be labelled{" "}
<strong>Processed Development Reference</strong>. Processed samples
are for visual reference; RRS commercially manufactures and supplies
greige fabric only. Finished appearance may vary depending on the
buyers processing and finishing method.
</p>
</div>
</aside>
</section>
);
}
+353
View File
@@ -0,0 +1,353 @@
.site-main {
margin-inline: var(--page-gutter);
outline: none;
}
.reveal {
opacity: 0;
clip-path: inset(0 0 12% 0);
transform: translateY(36px);
transition:
opacity 700ms ease,
clip-path 850ms cubic-bezier(0.22, 1, 0.36, 1),
transform 850ms cubic-bezier(0.22, 1, 0.36, 1);
will-change: opacity, clip-path, transform;
}
.reveal--visible {
opacity: 1;
clip-path: inset(0);
transform: translateY(0);
will-change: auto;
}
.skip-link {
position: fixed;
left: 20px;
top: -80px;
z-index: 100;
padding: 16px 24px;
background: var(--ink);
color: var(--paper);
}
.skip-link:focus {
top: 10px;
}
.site-header {
border-bottom: 1px solid var(--line);
}
.site-header__inner {
display: flex;
justify-content: space-between;
align-items: center;
min-height: 100px;
margin-inline: var(--page-gutter);
gap: 30px;
}
.brand {
display: inline-flex;
align-items: center;
gap: 16px;
text-decoration: none;
color: var(--ink);
}
.brand__mark {
font-family: var(--serif);
font-size: 42px;
font-weight: 600;
line-height: 1;
letter-spacing: -0.065em;
}
.brand__period {
color: var(--accent);
}
.brand__name {
border-left: 1px solid var(--line);
padding-left: 16px;
font-size: 9px;
line-height: 1.65;
text-transform: uppercase;
letter-spacing: 0.12em;
}
.site-nav {
display: flex;
align-items: center;
gap: 34px;
}
.site-nav__link {
position: relative;
padding: 14px 0;
color: var(--muted);
font-size: 12px;
text-decoration: none;
}
.site-nav__link--active {
color: var(--ink);
}
.site-nav__link--active::after {
content: "";
position: absolute;
bottom: 3px;
left: calc(50% - 2px);
width: 4px;
height: 4px;
background: var(--ink);
border-radius: 50%;
}
.site-nav__link:hover {
color: var(--ink);
}
.site-nav__contact {
display: flex;
align-items: center;
gap: 30px;
margin-left: 45px;
padding: 12px 20px;
border: 1px solid var(--ink);
border-radius: 30px;
color: var(--ink);
text-decoration: none;
font-size: 11px;
}
.site-nav__contact:hover {
background: var(--ink);
color: var(--paper);
}
.site-header__toggle {
display: none;
}
.site-footer {
margin-top: 100px;
padding: 55px var(--page-gutter) 24px;
background: var(--wash);
}
.site-footer__top {
display: grid;
grid-template-columns: 1.6fr 0.7fr 1fr;
gap: 60px;
}
.site-footer__name {
font-family: var(--serif);
font-size: 23px;
margin: 0 0 15px;
}
.site-footer__description {
max-width: 310px;
font-size: 13px;
color: var(--muted);
line-height: 1.8;
}
.site-footer__nav,
.site-footer__contact {
display: flex;
align-items: flex-start;
flex-direction: column;
gap: 12px;
}
.site-footer__label {
font-size: 13px;
font-weight: 500;
margin-bottom: 10px;
color: var(--muted);
}
.site-footer__link {
display: flex;
gap: 14px;
align-items: center;
color: var(--ink);
font-size: 12px;
text-decoration: none;
}
.site-footer__link:hover {
text-decoration: underline;
text-underline-offset: 4px;
}
.site-footer__pending {
margin: 0;
color: var(--muted);
font-size: 11px;
}
.site-footer__bottom {
display: flex;
justify-content: space-between;
align-items: flex-end;
padding-top: 60px;
}
.site-footer__wordmark {
font-family: var(--serif);
font-weight: 600;
font-size: clamp(110px, 18vw, 240px);
line-height: 0.88;
letter-spacing: -0.075em;
}
.site-footer__signature {
margin: 0 0 15px;
text-align: right;
font-family: var(--serif);
font-size: clamp(25px, 3vw, 40px);
line-height: 1.15;
letter-spacing: -0.03em;
}
.site-footer__legal {
display: flex;
justify-content: space-between;
gap: 20px;
margin-top: 38px;
padding-top: 23px;
border-top: 1px solid var(--line);
font-family: var(--mono);
font-size: 8px;
line-height: 1.6;
text-transform: uppercase;
letter-spacing: 0.02em;
color: var(--muted);
}
.site-footer__top-link {
color: var(--ink);
text-decoration: none;
}
.contact-banner {
position: relative;
overflow: hidden;
display: flex;
align-items: center;
min-height: 420px;
background: var(--ink);
color: var(--paper);
padding: 64px;
border-radius: 6px;
margin-top: 95px;
}
.contact-banner__copy {
position: relative;
z-index: 1;
max-width: 650px;
}
.contact-banner__label {
margin: 0 0 24px;
font-size: 14px;
font-weight: 500;
color: #c2c3b8;
}
.contact-banner__title {
margin: 0 0 22px;
font-family: var(--serif);
font-size: clamp(36px, 4.2vw, 60px);
font-weight: 400;
line-height: 1.1;
letter-spacing: -0.04em;
}
.contact-banner__text {
max-width: 440px;
color: #c2c3b8;
font-size: 14px;
line-height: 1.9;
margin: 0 0 30px;
}
.contact-banner__motif {
position: absolute;
display: flex;
gap: 25px;
right: -60px;
bottom: -80px;
transform: rotate(30deg);
}
.contact-banner__loop {
display: block;
width: 100px;
height: 430px;
border: 1px solid #74776955;
border-radius: 50%;
}
@media (prefers-reduced-motion: reduce) {
.reveal,
.reveal--visible {
opacity: 1;
clip-path: none;
transform: none;
transition: none;
}
}
@media (max-width: 950px) {
.site-nav {
gap: 23px;
}
.site-nav__contact {
margin-left: 12px;
}
.site-footer__top {
gap: 30px;
}
}
@media (max-width: 760px) {
.site-header__inner {
min-height: 80px;
flex-wrap: wrap;
gap: 0;
}
.brand__mark {
font-size: 35px;
}
.brand__name {
font-size: 8px;
}
.site-header__toggle {
display: flex;
justify-content: space-between;
gap: 25px;
align-items: center;
min-height: 44px;
padding: 8px 0 8px 15px;
border: 0;
color: var(--ink);
background: transparent;
font-size: 12px;
cursor: pointer;
}
.site-nav {
display: none;
width: 100%;
padding: 14px 0 24px;
}
.site-nav--open {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 8px 24px;
}
.site-nav__link {
padding: 15px 4px;
}
.site-nav__link--active::after {
left: auto;
right: 15px;
bottom: 20px;
}
.site-nav__contact {
grid-column: 1 / -1;
margin: 12px 0 0;
justify-content: space-between;
}
.contact-banner {
padding: 40px 25px;
min-height: 400px;
margin-top: 60px;
}
.site-footer {
padding: 40px 20px 24px;
margin-top: 65px;
}
.site-footer__top {
grid-template-columns: 1fr 1fr;
gap: 32px 20px;
}
.site-footer__intro {
grid-column: 1 / -1;
}
.site-footer__bottom {
padding-top: 45px;
gap: 15px;
}
.site-footer__legal {
flex-wrap: wrap;
}
.site-footer__signature {
font-size: 24px;
}
}
+213
View File
@@ -0,0 +1,213 @@
import { useEffect, useState } from "react";
import { Link, NavLink, Outlet, useLocation } from "react-router-dom";
import { company, navigation } from "../data";
import { Arrow, WhatsAppAction } from "./Primitives";
import "./Layout.css";
function Brand() {
return (
<Link className="brand" to="/" aria-label="RRS home">
<span className="brand__mark">
RRS<span className="brand__period">.</span>
</span>
<span className="brand__name">
Radhe Radhe
<br />
Synthetics
</span>
</Link>
);
}
function Header() {
const [menuOpen, setMenuOpen] = useState(false);
const closeMenu = () => setMenuOpen(false);
return (
<header
className="site-header"
onKeyDown={(event) => {
if (menuOpen && event.key === "Escape") {
closeMenu();
document.getElementById("menu-toggle")?.focus();
}
}}
>
<div className="site-header__inner">
<Brand />
<button
id="menu-toggle"
className="site-header__toggle"
type="button"
aria-controls="primary-navigation"
aria-expanded={menuOpen}
onClick={() => setMenuOpen((open) => !open)}
>
{menuOpen ? "Close" : "Menu"}
<span aria-hidden="true">{menuOpen ? "" : "+"}</span>
</button>
<nav
id="primary-navigation"
aria-label="Main navigation"
className={`site-nav${menuOpen ? " site-nav--open" : ""}`}
>
{navigation.map(({ to, label }) => (
<NavLink
key={to}
to={to}
end={to === "/"}
onClick={closeMenu}
className={({ isActive }) =>
`site-nav__link${isActive ? " site-nav__link--active" : ""}`
}
>
{label}
</NavLink>
))}
<Link
className="site-nav__contact"
to="/contact#whatsapp"
onClick={closeMenu}
>
Let's talk <Arrow diagonal />
</Link>
</nav>
</div>
</header>
);
}
function Footer() {
return (
<footer className="site-footer">
<div className="site-footer__top">
<div className="site-footer__intro">
<p className="site-footer__name">{company.name}</p>
<p className="site-footer__description">{company.description}</p>
</div>
<nav aria-label="Footer navigation" className="site-footer__nav">
<span className="site-footer__label">Explore</span>
{navigation.map(({ to, label }) => (
<Link className="site-footer__link" to={to} key={to}>
{label}
</Link>
))}
</nav>
<div className="site-footer__contact">
<span className="site-footer__label">Start a conversation</span>
<Link className="site-footer__link" to="/contact#whatsapp">
WhatsApp <Arrow diagonal />
</Link>
<Link className="site-footer__link" to="/contact#direct-contact">
Phone · Email · Address <Arrow diagonal />
</Link>
<p className="site-footer__pending">
Contact details to be supplied.
</p>
</div>
</div>
<div className="site-footer__bottom">
<span className="site-footer__wordmark" aria-hidden="true">
RRS.
</span>
<p className="site-footer__signature">
Built to develop.
<br />
<em>Built to repeat.</em>
</p>
</div>
<div className="site-footer__legal">
<span>
© {new Date().getFullYear()} Radhe Radhe Synthetics Pvt. Ltd.
</span>
<span>Commercial supply: greige fabric only.</span>
<a className="site-footer__top-link" href="#top">
Back to top
</a>
</div>
</footer>
);
}
export function ContactBanner({
title = (
<>
Developing your
<br />
<em>next construction?</em>
</>
),
text = "Talk to RRS about fabric structure, yarn combinations, bulk requirements or new developments.",
}) {
return (
<section className="contact-banner">
<div className="contact-banner__copy">
<p className="contact-banner__label">
A conversation is where it begins
</p>
<h2 className="contact-banner__title">{title}</h2>
<p className="contact-banner__text">{text}</p>
<WhatsAppAction variant="light" />
</div>
<div className="contact-banner__motif" aria-hidden="true">
<span className="contact-banner__loop" />
<span className="contact-banner__loop" />
<span className="contact-banner__loop" />
</div>
</section>
);
}
export default function Layout() {
const { pathname, hash, key } = useLocation();
useEffect(() => {
const title =
navigation.find(({ to }) => to === pathname)?.label ?? "Page not found";
document.title = `${title === "Home" ? "RRS" : `${title} — RRS`} | Radhe Radhe Synthetics`;
const target = hash
? document.getElementById(hash.slice(1))
: document.getElementById("main-content");
if (hash && target) target.scrollIntoView({ behavior: "instant" });
else window.scrollTo({ top: 0, behavior: "instant" });
target?.focus({ preventScroll: true });
}, [pathname, hash, key]);
useEffect(() => {
const sections = [...document.querySelectorAll("#main-content > section")];
const reducedMotion = window.matchMedia(
"(prefers-reduced-motion: reduce)",
).matches;
if (reducedMotion || !("IntersectionObserver" in window)) {
sections.forEach((section) => section.classList.add("reveal--visible"));
return undefined;
}
sections.forEach((section) => section.classList.add("reveal"));
const observer = new IntersectionObserver(
(entries) => {
entries.forEach((entry) => {
if (!entry.isIntersecting) return;
entry.target.classList.add("reveal--visible");
observer.unobserve(entry.target);
});
},
{ threshold: 0.08, rootMargin: "0px 0px -8% 0px" },
);
sections.forEach((section) => observer.observe(section));
return () => observer.disconnect();
}, [pathname, key]);
return (
<div className="site-shell" id="top">
<a className="skip-link" href="#main-content">
Skip to content
</a>
<Header key={key} />
<main className="site-main" id="main-content" tabIndex={-1}>
<Outlet />
</main>
<Footer />
</div>
);
}
+194
View File
@@ -0,0 +1,194 @@
.arrow {
flex: 0 0 auto;
}
.action {
display: inline-flex;
align-items: center;
justify-content: space-between;
gap: 30px;
min-height: 48px;
padding: 14px 23px;
border: 1px solid transparent;
border-radius: 100px;
font-size: 12px;
font-weight: 600;
text-decoration: none;
cursor: pointer;
transition:
background 180ms ease,
color 180ms ease;
}
.action--dark {
background: var(--ink);
color: var(--paper);
}
.action--light {
background: var(--paper);
color: var(--ink);
}
.action--outline {
background: transparent;
color: var(--ink);
border-color: var(--line);
}
.action--ghost {
background: transparent;
color: var(--paper);
border-color: #ffffff60;
}
.action--dark:hover:not(:disabled) {
background: #474b3f;
}
.action--light:hover {
background: #dedbcf;
}
.action--outline:hover {
background: var(--wash);
}
.action--ghost:hover {
background: #ffffff15;
}
.action:disabled {
cursor: not-allowed;
opacity: 0.55;
}
.eyebrow {
margin: 0 0 18px;
font-family: var(--sans);
font-size: 14px;
line-height: 1.6;
font-weight: 500;
}
.section-heading {
display: flex;
align-items: flex-end;
justify-content: space-between;
gap: 45px;
margin-bottom: 44px;
}
.section-heading__title {
max-width: 680px;
}
.section-heading__heading {
margin: 0;
font-family: var(--serif);
font-size: clamp(32px, 3.6vw, 50px);
line-height: 1.12;
font-weight: 400;
letter-spacing: -0.045em;
}
.section-heading__aside {
max-width: 360px;
color: var(--muted);
font-size: 14px;
line-height: 1.8;
}
.section-heading--centered {
display: block;
text-align: center;
}
.section-heading--centered .section-heading__title {
max-width: none;
}
.section-heading--centered .section-heading__aside {
margin: 24px auto 0;
}
.textile {
position: relative;
min-width: 0;
margin: 0;
overflow: hidden;
border-radius: 5px;
background: #aba08b;
}
.textile__image {
display: block;
width: 100%;
height: 100%;
min-height: 300px;
object-fit: cover;
transition: transform 700ms ease;
}
.textile:hover .textile__image {
transform: scale(1.035);
}
.textile__caption {
position: absolute;
left: 18px;
bottom: 16px;
right: 18px;
width: fit-content;
padding: 7px 10px;
border-radius: 2px;
background: #f5f3eeeb;
color: #34352d;
font-size: 11px;
line-height: 1.5;
}
.page-hero {
display: grid;
grid-template-columns: 1.15fr 1fr;
gap: 70px;
padding: 66px 0 74px;
}
.page-hero__copy {
display: flex;
flex-direction: column;
align-items: flex-start;
padding: 22px 0 0;
}
.page-hero__title {
margin: 6px 0 28px;
font-family: var(--serif);
font-weight: 400;
font-size: clamp(48px, 5.6vw, 78px);
letter-spacing: -0.055em;
line-height: 1.04;
}
.page-hero__description {
max-width: 470px;
color: var(--muted);
font-size: 16px;
line-height: 1.9;
}
.page-hero__footnote {
display: block;
padding-top: 46px;
margin-top: auto;
font-size: 12px;
color: var(--muted);
}
@media (max-width: 760px) {
.section-heading {
align-items: flex-start;
flex-direction: column;
gap: 22px;
margin-bottom: 30px;
}
.page-hero {
grid-template-columns: 1fr;
gap: 30px;
padding: 35px 0 50px;
}
.page-hero__footnote {
padding-top: 26px;
}
.page-hero__title {
font-size: clamp(44px, 10vw, 70px);
}
.page-hero__copy {
padding: 0;
}
.textile__image {
max-height: 460px;
}
}
@media (prefers-reduced-motion: reduce) {
.action,
.textile__image {
transition: none;
}
.textile:hover .textile__image {
transform: none;
}
}
+125
View File
@@ -0,0 +1,125 @@
import { Link } from "react-router-dom";
import { company, whatsappUrl } from "../data";
import "./Primitives.css";
export function Arrow({ diagonal = false }) {
return (
<svg
className="arrow"
width="18"
height="18"
viewBox="0 0 24 24"
fill="none"
aria-hidden="true"
>
<path
d={diagonal ? "M6 18 18 6M6 6h12v12" : "M4 12h15m-6-6 6 6-6 6"}
stroke="currentColor"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
);
}
export function Action({ children, to, variant = "dark" }) {
return (
<Link className={`action action--${variant}`} to={to}>
{children}
<Arrow />
</Link>
);
}
export function WhatsAppAction({
children = "Talk to us on WhatsApp",
variant = "dark",
direct = false,
}) {
const url = whatsappUrl(company.whatsapp);
const className = `action action--${variant}`;
if (url)
return (
<a className={className} href={url} target="_blank" rel="noreferrer">
{children}
<Arrow diagonal />
</a>
);
if (direct)
return (
<button
className={className}
type="button"
disabled
aria-describedby="whatsapp-status"
>
{children}
<Arrow diagonal />
</button>
);
return (
<Action to="/contact#whatsapp" variant={variant}>
{children}
</Action>
);
}
export function Eyebrow({ children }) {
return <p className="eyebrow">{children}</p>;
}
export function SectionHeading({ label, title, children, centered = false }) {
return (
<header
className={`section-heading${centered ? " section-heading--centered" : ""}`}
>
<div className="section-heading__title">
<Eyebrow>{label}</Eyebrow>
<h2 className="section-heading__heading">{title}</h2>
</div>
{children && <div className="section-heading__aside">{children}</div>}
</header>
);
}
export function Textile({
kind = "natural",
className = "",
caption = true,
eager = false,
}) {
return (
<figure className={`textile textile--${kind} ${className}`}>
<img
className="textile__image"
src={`/images/knit-${kind}.jpg`}
alt="Illustrative knitted texture — not an RRS product photograph"
width="1600"
height="1200"
loading={eager ? "eager" : "lazy"}
/>
{caption && (
<figcaption className="textile__caption">
Illustrative texture · RRS photography to follow
</figcaption>
)}
</figure>
);
}
export function PageHero({ label, title, children, texture = "natural" }) {
return (
<section className="page-hero">
<div className="page-hero__copy">
<Eyebrow>{label}</Eyebrow>
<h1 className="page-hero__title">{title}</h1>
<div className="page-hero__description">{children}</div>
<span className="page-hero__footnote">
Radhe Radhe Synthetics · Greige knitted fabrics
</span>
</div>
<Textile kind={texture} eager />
</section>
);
}
+56
View File
@@ -0,0 +1,56 @@
.stats {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 32px;
border-block: 1px solid var(--line);
padding: 42px 0;
margin-top: 78px;
}
.stats__item {
min-width: 0;
}
.stats__label {
margin: 0 0 26px;
font-size: 13px;
font-weight: 500;
line-height: 1.6;
color: var(--muted);
}
.stats__value {
font-family: var(--serif);
font-size: clamp(38px, 4vw, 54px);
letter-spacing: -0.035em;
line-height: 1.1;
margin: 0 0 10px;
}
.stats__unit {
font-size: 12px;
font-weight: 500;
line-height: 1.6;
margin: 0 0 16px;
}
.stats__text {
max-width: 230px;
font-size: 13px;
color: var(--muted);
line-height: 1.8;
margin: 0;
}
@media (max-width: 850px) {
.stats {
grid-template-columns: 1fr 1fr;
gap: 36px 24px;
}
}
@media (max-width: 480px) {
.stats {
margin-top: 45px;
padding: 30px 0;
}
.stats__value {
font-size: 36px;
}
.stats__label {
margin-bottom: 18px;
}
}
+44
View File
@@ -0,0 +1,44 @@
import CapacityValue from "./CapacityValue";
import "./Stats.css";
const facts = [
{
label: "Production capacity",
value: <CapacityValue />,
unit: "Tonnes / month",
text: "Approximate monthly manufacturing capacity.",
},
{
label: "Our specialisation",
value: "Greige",
unit: "Knitted fabrics only",
text: "The starting point for a world of possibilities.",
},
{
label: "The capability",
value: "Circular",
unit: "Knitting · multiple constructions",
text: "Different structures. Different configurations.",
},
{
label: "The approach",
value: "Continuous",
unit: "Fabric development",
text: "Exploring yarns, blends and new constructions.",
},
];
export default function Stats() {
return (
<section className="stats" aria-label="RRS at a glance">
{facts.map(({ label, value, unit, text }) => (
<div className="stats__item" key={label}>
<p className="stats__label">{label}</p>
<p className="stats__value">{value}</p>
<p className="stats__unit">{unit}</p>
<p className="stats__text">{text}</p>
</div>
))}
</section>
);
}
+82
View File
@@ -0,0 +1,82 @@
export const company = Object.freeze({
name: "Radhe Radhe Synthetics Pvt. Ltd.",
description:
"Greige knitted fabrics engineered for diverse apparel applications.",
whatsapp: "", // Supply the business number in international digits, without + or spaces.
phone: "",
email: "",
address: "",
mapsUrl: "",
});
export const navigation = [
{ to: "/", label: "Home" },
{ to: "/fabrics", label: "Fabrics" },
{ to: "/about", label: "About" },
{ to: "/contact", label: "Contact" },
];
export const capabilities = [
{
title: "Develop",
subtitle: "Continuous fabric development",
text: "Different structures call for different yarns, blends and machine configurations. RRS continually develops new knitted constructions for evolving apparel requirements.",
},
{
title: "Repeat",
subtitle: "Consistency in bulk production",
text: "A successful development must remain dependable when production scales. RRS focuses on maintaining repeatability and quality across bulk manufacturing.",
},
{
title: "Manufacture",
subtitle: "Extensive circular-knitting capability",
text: "A broad range of circular-knitting configurations allows RRS to work across different structures, materials and fabric characteristics.",
},
];
export const applications = [
{
title: "Shirting constructions",
text: "Greige knitted fabrics developed for shirt and apparel applications.",
},
{
title: "Textured knits",
text: "Knitted constructions where yarn and machine configuration are used to create distinctive surface character.",
},
{
title: "Structured knits",
text: "More pronounced knitted structures developed using different yarn combinations and knitting setups.",
},
{
title: "Developed constructions",
text: "Fabric developments created around specific visual, structural or material requirements.",
},
];
export const yarns = [
"Polyester",
"Cotton",
"Rayon",
"Stretch yarns",
"Lycra / elastane combinations",
"Specialty yarn combinations",
"Blended yarn constructions",
"Other yarns selected according to fabric requirements",
];
// Development names are references, not technical categories or specifications.
export const developments = [
"Micro Break",
"Luxe",
"Jerry",
"Killer",
"Joker",
"Ice Cool",
"Maple",
];
export function whatsappUrl(number) {
return /^[1-9]\d{7,14}$/.test(number)
? `https://wa.me/${number}?text=${encodeURIComponent("Hello RRS, I'd like to discuss a fabric requirement.")}`
: null;
}
+39
View File
@@ -0,0 +1,39 @@
import React from "react";
import { createRoot } from "react-dom/client";
import { BrowserRouter, Route, Routes } from "react-router-dom";
import Layout from "./components/Layout";
import { Action } from "./components/Primitives";
import Home from "./pages/Home";
import Fabrics from "./pages/Fabrics";
import About from "./pages/About";
import Contact from "./pages/Contact";
import "./base.css";
function NotFound() {
return (
<section className="not-found">
<p className="not-found__label">404 / Page not found</p>
<h1 className="not-found__title">A loose thread.</h1>
<p className="not-found__text">
This page doesnt exist. Lets return to the beginning.
</p>
<Action to="/">Back to home</Action>
</section>
);
}
createRoot(document.getElementById("root")).render(
<React.StrictMode>
<BrowserRouter>
<Routes>
<Route element={<Layout />}>
<Route index element={<Home />} />
<Route path="fabrics" element={<Fabrics />} />
<Route path="about" element={<About />} />
<Route path="contact" element={<Contact />} />
<Route path="*" element={<NotFound />} />
</Route>
</Routes>
</BrowserRouter>
</React.StrictMode>,
);
+263
View File
@@ -0,0 +1,263 @@
.philosophy {
border-top: 1px solid var(--line);
padding-top: 70px;
}
.philosophy__grid {
display: grid;
grid-template-columns: 1fr 1fr;
border-block: 1px solid var(--line);
padding-block: 48px;
}
.philosophy__item {
padding: 10px clamp(24px, 4vw, 72px);
}
.philosophy__item:first-child {
text-align: right;
}
.philosophy__item + .philosophy__item {
border-left: 1px solid var(--line);
}
.philosophy__title {
margin: 0 0 18px;
font-family: var(--serif);
font-weight: 400;
font-size: clamp(40px, 4vw, 58px);
letter-spacing: -0.04em;
}
.philosophy__text {
max-width: 390px;
margin: 0;
font-size: 14px;
line-height: 1.9;
color: var(--muted);
}
.philosophy__item:first-child .philosophy__text {
margin-left: auto;
}
.leadership {
padding-top: 100px;
}
.leadership__grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 28px;
}
.owner {
margin: 0;
}
.owner__portrait {
display: flex;
flex-direction: column;
align-items: center;
justify-content: space-between;
position: relative;
min-height: 370px;
padding: 24px;
background: #e5e2d9;
color: #7e8271;
border: 1px solid var(--line);
border-radius: 4px;
}
.owner__image-label {
align-self: flex-start;
font-size: 12px;
font-weight: 500;
color: var(--muted);
}
.owner__silhouette {
width: 185px;
height: 220px;
}
.owner__pending {
font-size: 11px;
text-align: center;
color: var(--muted);
}
.owner__caption {
padding: 22px 0 0;
}
.owner__identity {
display: flex;
align-items: baseline;
gap: 20px;
justify-content: space-between;
}
.owner__name {
margin: 0;
font-family: var(--serif);
font-weight: 400;
font-size: 26px;
}
.owner__role {
color: var(--muted);
font-size: 11px;
}
.owner__bio {
max-width: 370px;
margin: 13px 0 0;
font-size: 13px;
color: var(--muted);
line-height: 1.9;
}
.approach {
padding-top: 100px;
}
.approach__grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 35px;
}
.approach__step {
border-top: 1px solid var(--ink);
padding-top: 20px;
}
.approach__title {
margin: 8px 0 18px;
font-family: var(--serif);
font-size: 36px;
font-weight: 400;
letter-spacing: -0.04em;
}
.approach__text {
font-size: 13px;
line-height: 1.9;
color: var(--muted);
margin: 0;
}
.location {
padding-top: 100px;
}
.location__grid {
display: grid;
grid-template-columns: 1.4fr 1fr;
gap: 50px;
}
.location__map {
position: relative;
display: grid;
place-items: center;
min-height: 360px;
border: 1px solid #d2d5c7;
border-radius: 4px;
overflow: hidden;
background-color: #e4e6dc;
background-image:
linear-gradient(#75866b16 1px, transparent 1px),
linear-gradient(90deg, #75866b16 1px, transparent 1px);
background-size: 40px 40px;
}
.location__map-label {
position: absolute;
top: 20px;
left: 20px;
right: 20px;
font-size: 11px;
line-height: 1.6;
color: var(--muted);
}
.location__pin {
display: flex;
align-items: center;
gap: 12px;
border: 1px solid var(--ink);
padding: 14px 21px;
background: var(--paper);
border-radius: 50px;
font-family: var(--serif);
font-size: 24px;
}
.location__pin-dot {
width: 9px;
height: 9px;
background: var(--accent);
border-radius: 50%;
outline: 1px solid #8f725c50;
outline-offset: 5px;
margin-right: 4px;
}
.location__map-caption {
position: absolute;
bottom: 22px;
font-family: var(--serif);
font-style: italic;
font-size: 18px;
color: var(--muted);
}
.location__details {
align-self: center;
}
.location__name {
max-width: 360px;
margin: 0 0 25px;
font-family: var(--serif);
font-size: 31px;
font-weight: 400;
line-height: 1.2;
letter-spacing: -0.025em;
}
.location__address {
font-size: 14px;
line-height: 1.9;
color: var(--muted);
margin-bottom: 25px;
}
.location__button {
display: inline-block;
padding: 15px 20px;
border: 1px solid var(--ink);
background: var(--ink);
color: var(--paper);
border-radius: 30px;
font-size: 11px;
text-decoration: none;
}
.location__button:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.location__status {
font-size: 11px;
color: var(--muted);
line-height: 1.8;
}
@media (max-width: 650px) {
.philosophy {
padding-top: 45px;
}
.philosophy__grid,
.leadership__grid,
.approach__grid,
.location__grid {
grid-template-columns: 1fr;
}
.philosophy__grid {
padding-block: 0;
}
.philosophy__item {
padding: 32px 0;
}
.philosophy__item:first-child {
text-align: left;
}
.philosophy__item:first-child .philosophy__text {
margin-left: 0;
}
.philosophy__item + .philosophy__item {
border-top: 1px solid var(--line);
border-left: 0;
}
.leadership,
.approach,
.location {
padding-top: 60px;
}
.leadership__grid {
gap: 35px;
}
.owner__portrait {
min-height: 330px;
}
.location__grid {
gap: 30px;
}
}
+232
View File
@@ -0,0 +1,232 @@
import { company } from "../data";
import { Eyebrow, PageHero, SectionHeading } from "../components/Primitives";
import { ContactBanner } from "../components/Layout";
import "./About.css";
const principles = [
{
title: "Built to repeat.",
text: "A fabric developed successfully should remain dependable when manufactured at scale.",
},
{
title: "Built to develop.",
text: "New yarn combinations, structures and constructions create new possibilities for apparel.",
},
];
const process = [
{
title: "Develop.",
text: "A construction begins with the relationship between yarn, machine and structure, with the eventual application in mind.",
},
{
title: "Test.",
text: "Selected constructions are evaluated through processed reference samples to explore their possibilities.",
},
{
title: "Repeat.",
text: "Successful developments are reproduced consistently in commercial greige production.",
},
];
function PortraitPlaceholder({ number }) {
return (
<figure className="owner">
<div
className="owner__portrait"
role="img"
aria-label={`Owner ${number} portrait placeholder; professional photograph to be supplied`}
>
<span className="owner__image-label">Portrait placeholder</span>
<svg
className="owner__silhouette"
viewBox="0 0 200 230"
fill="none"
aria-hidden="true"
>
<circle
cx="100"
cy="76"
r="35"
stroke="currentColor"
strokeWidth="1"
/>
<path
d="M28 218v-29c0-40 32-72 72-72s72 32 72 72v29"
stroke="currentColor"
/>
<path
d="M8 8h26M8 8v26m184-26h-26m26 0v26M8 222h26M8 222v-26m184 26h-26m26 0v-26"
stroke="currentColor"
/>
</svg>
<span className="owner__pending">
Professional photograph to follow
</span>
</div>
<figcaption className="owner__caption">
<div className="owner__identity">
<h3 className="owner__name">[Owner Name]</h3>
<span className="owner__role">[Director / Role]</span>
</div>
<p className="owner__bio">
Biography to be supplied. This profile will introduce the person
behind the fabric.
</p>
</figcaption>
</figure>
);
}
export default function About() {
return (
<>
<PageHero
label="About RRS"
title={
<>
Built around
<br />
<em>the fabric.</em>
</>
}
>
{" "}
<p>
Radhe Radhe Synthetics is a specialist manufacturer of greige knitted
fabrics with a focus on consistent bulk production and continuous
fabric development.
</p>
</PageHero>
<section className="philosophy">
<SectionHeading
centered
label="Our philosophy"
title={
<>
Consistency
<br />
<em>and curiosity.</em>
</>
}
>
<p>
Two ideas guide the way RRS works. Together, they define how we
approach knitting and production.
</p>
</SectionHeading>
<div className="philosophy__grid">
{principles.map(({ title, text }) => (
<article className="philosophy__item" key={title}>
<h3 className="philosophy__title">{title}</h3>
<p className="philosophy__text">{text}</p>
</article>
))}
</div>
</section>
<section className="leadership">
<SectionHeading
label="Our people"
title={
<>
The people
<br />
<em>behind RRS.</em>
</>
}
>
<p>
Meet our owners and directors.
<br />
Names, roles and biographies will be added when supplied.
</p>
</SectionHeading>
<div className="leadership__grid">
<PortraitPlaceholder number={1} />
<PortraitPlaceholder number={2} />
</div>
</section>
<section className="approach">
<SectionHeading
label="Our manufacturing approach"
title={
<>
Develop. Test. <em>Repeat.</em>
</>
}
/>
<div className="approach__grid">
{process.map(({ title, text }) => (
<article className="approach__step" key={title}>
<h3 className="approach__title">{title}</h3>
<p className="approach__text">{text}</p>
</article>
))}
</div>
</section>
<section className="location">
<SectionHeading
label="Our location"
title={
<>
Where <em>we work.</em>
</>
}
/>
<div className="location__grid">
<div
className="location__map"
role="img"
aria-label="Map placeholder. Verified factory or office address has not yet been supplied."
>
<span className="location__map-label">
Map placeholder / Not a geographical location
</span>
<div className="location__pin">
<span className="location__pin-dot" />
<span>RRS</span>
</div>
<span className="location__map-caption">
A place for our next conversation.
</span>
</div>
<div className="location__details">
<Eyebrow>Factory or office</Eyebrow>
<h3 className="location__name">{company.name}</h3>
<p className="location__address">
{company.address || (
<>
[Full Factory / Office Address]
<br />
[City, State, PIN, India]
</>
)}
</p>
{company.mapsUrl ? (
<a
className="location__button"
href={company.mapsUrl}
target="_blank"
rel="noreferrer"
>
Open in Google Maps
</a>
) : (
<button
className="location__button"
type="button"
disabled
aria-describedby="map-status"
>
Open in Google Maps
</button>
)}
<p className="location__status" id="map-status">
Verified location details to be supplied.
</p>
</div>
</div>
</section>
<ContactBanner />
</>
);
}
+148
View File
@@ -0,0 +1,148 @@
.whatsapp {
display: grid;
grid-template-columns: 1.5fr 1fr;
align-items: center;
gap: 50px;
border-top: 2px solid var(--ink);
padding: 48px;
background: #fbfaf7;
border-radius: 5px;
scroll-margin-top: 24px;
}
.whatsapp__title {
margin: 0 0 22px;
font-family: var(--serif);
font-size: clamp(32px, 3.8vw, 48px);
font-weight: 400;
line-height: 1.13;
letter-spacing: -0.04em;
}
.whatsapp__text {
max-width: 460px;
font-size: 14px;
line-height: 1.9;
color: var(--muted);
}
.whatsapp__message {
margin: 28px 0 0;
padding: 18px 22px;
border-left: 2px solid var(--accent);
background: var(--wash);
font-family: var(--serif);
font-size: 20px;
font-style: italic;
line-height: 1.5;
}
.whatsapp__clipboard {
display: flex;
align-items: flex-start;
flex-direction: column;
gap: 8px;
margin-top: 12px;
}
.whatsapp__copy-button {
display: inline-flex;
gap: 18px;
align-items: center;
min-height: 44px;
padding: 8px 0;
border: none;
background: transparent;
color: var(--ink);
font-size: 11px;
cursor: pointer;
}
.whatsapp__copy-button:hover {
text-decoration: underline;
text-underline-offset: 5px;
}
.whatsapp__copy-status {
font-size: 11px;
color: var(--muted);
line-height: 1.6;
}
.whatsapp__action {
display: flex;
align-items: center;
flex-direction: column;
text-align: center;
}
.whatsapp__icon {
display: block;
width: 58px;
height: 58px;
margin-bottom: 28px;
}
.whatsapp__status {
max-width: 270px;
margin: 18px 0 0;
font-size: 11px;
color: var(--muted);
line-height: 1.8;
}
.channels {
padding-top: 100px;
scroll-margin-top: 25px;
}
.channels__grid {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 30px;
}
.channels__item {
min-width: 0;
border-top: 1px solid var(--line);
padding-top: 20px;
}
.channels__title {
margin: 8px 0 16px;
font-size: 13px;
color: var(--muted);
font-weight: 500;
}
.channels__value {
display: block;
font-family: var(--serif);
font-size: 24px;
color: var(--ink);
text-decoration: none;
line-height: 1.4;
margin: 0;
overflow-wrap: anywhere;
}
.channels__note {
font-size: 12px;
color: var(--muted);
line-height: 1.9;
}
@media (max-width: 900px) {
.whatsapp {
grid-template-columns: 1fr;
gap: 35px;
padding: 32px;
}
.whatsapp__action {
align-items: flex-start;
text-align: left;
}
.channels__grid {
grid-template-columns: 1fr 1fr;
}
}
@media (max-width: 600px) {
.whatsapp {
padding: 30px 22px;
}
.whatsapp__message {
padding: 16px;
}
.channels {
padding-top: 60px;
}
.channels__grid {
gap: 25px;
}
.channels__value {
font-size: 21px;
}
}
+165
View File
@@ -0,0 +1,165 @@
import { useState } from "react";
import { company } from "../data";
import {
Eyebrow,
PageHero,
SectionHeading,
WhatsAppAction,
} from "../components/Primitives";
import { ContactBanner } from "../components/Layout";
import "./Contact.css";
const message = "Hello RRS, I'd like to discuss a fabric requirement.";
export default function Contact() {
const [copyStatus, setCopyStatus] = useState("");
async function copyMessage() {
try {
await navigator.clipboard.writeText(message);
setCopyStatus("Message copied.");
} catch {
setCopyStatus(
"Could not copy automatically. Please select and copy the message above.",
);
}
}
const channels = [
{
title: "WhatsApp",
value: company.whatsapp || "[Number]",
href: "#whatsapp",
note: "Discuss a fabric requirement.",
},
{
title: "Phone",
value: company.phone || "[Number]",
href: company.phone ? `tel:${company.phone}` : null,
note: "A direct conversation with RRS.",
},
{
title: "Email",
value: company.email || "[Email Address]",
href: company.email ? `mailto:${company.email}` : null,
note: "Share your requirement or reference.",
},
{
title: "Address",
value: company.address || "[Full Business Address]",
href: company.mapsUrl || null,
note: "Factory / office location.",
},
];
return (
<>
<PageHero
label="Contact RRS"
title={
<>
Lets talk
<br />
<em>fabric.</em>
</>
}
texture="structured"
>
<p>
Whether youre discussing an existing requirement, bulk production or
a new fabric construction, connect directly with RRS.
</p>
</PageHero>
<section className="whatsapp" id="whatsapp" tabIndex={-1}>
<div className="whatsapp__copy">
<Eyebrow>A direct conversation</Eyebrow>
<h2 className="whatsapp__title">
Your next construction.
<br />
<em>Lets begin here.</em>
</h2>
<p className="whatsapp__text">
Share your fabric requirement, intended application, composition or
reference with our team.
</p>
<blockquote className="whatsapp__message">{message}</blockquote>
<div className="whatsapp__clipboard">
<button
className="whatsapp__copy-button"
type="button"
onClick={copyMessage}
>
Copy conversation starter <span aria-hidden="true"></span>
</button>
<span className="whatsapp__copy-status" role="status">
{copyStatus}
</span>
</div>
</div>
<div className="whatsapp__action">
<svg
className="whatsapp__icon"
viewBox="0 0 48 48"
fill="none"
aria-hidden="true"
>
<path
d="M40 23a16 16 0 0 1-24 14L7 40l3-9A16 16 0 1 1 40 23Z"
stroke="currentColor"
strokeWidth="1.5"
/>
<path
d="M17 17c1 7 6 12 13 14l3-4-5-3-2 2c-3-1-5-3-6-6l2-2-3-4-2 3Z"
stroke="currentColor"
strokeWidth="1.5"
strokeLinejoin="round"
/>
</svg>
<WhatsAppAction direct />
<p className="whatsapp__status" id="whatsapp-status">
{company.whatsapp
? "Opens WhatsApp with a conversation starter."
: "Business WhatsApp number to be supplied. Chat will be available once details are confirmed."}
</p>
</div>
</section>
<section className="channels" id="direct-contact" tabIndex={-1}>
<SectionHeading
label="Direct contact"
title={
<>
A line <em>to RRS.</em>
</>
}
>
<p>
Contact information is awaiting confirmation. No phone numbers,
email addresses or locations have been assumed.
</p>
</SectionHeading>
<div className="channels__grid">
{channels.map(({ title, value, href, note }) => (
<article className="channels__item" key={title}>
<h3 className="channels__title">{title}</h3>
{href ? (
<a className="channels__value" href={href}>
{value}
</a>
) : (
<p className="channels__value">{value}</p>
)}
<p className="channels__note">{note}</p>
</article>
))}
</div>
</section>
<ContactBanner
title={
<>
Have a construction
<br />
<em>in mind?</em>
</>
}
text="Share your fabric requirement, intended application, composition or reference with our team and start a conversation about what can be developed."
/>
</>
);
}
+232
View File
@@ -0,0 +1,232 @@
.applications {
border-top: 1px solid var(--line);
padding-top: 70px;
scroll-margin-top: 30px;
}
.applications__list {
border-top: 1px solid var(--line);
}
.applications__item {
border-bottom: 1px solid var(--line);
}
.applications__summary {
display: flex;
align-items: center;
gap: 30px;
padding: 25px 4px;
list-style: none;
cursor: pointer;
}
.applications__summary::-webkit-details-marker {
display: none;
}
.applications__title {
font-family: var(--serif);
font-size: clamp(24px, 2.6vw, 35px);
letter-spacing: -0.03em;
}
.applications__toggle {
margin-left: auto;
width: 28px;
height: 28px;
border: 1px solid var(--line);
border-radius: 50%;
display: grid;
place-items: center;
flex: 0 0 auto;
}
.applications__toggle::after {
content: "+";
font-size: 18px;
font-weight: 300;
}
.applications__item[open] .applications__toggle::after {
content: "";
}
.applications__text {
max-width: 680px;
margin: 0 40px 28px 4px;
color: var(--muted);
font-size: 14px;
line-height: 1.9;
}
.materials {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 100px;
padding: 100px 0;
}
.materials__title {
font-family: var(--serif);
font-size: clamp(34px, 4vw, 52px);
line-height: 1.13;
letter-spacing: -0.045em;
font-weight: 400;
margin: 0 0 28px;
}
.materials__text {
font-size: 14px;
color: var(--muted);
line-height: 1.9;
max-width: 400px;
}
.materials__note {
display: block;
margin-top: 26px;
font-size: 12px;
font-weight: 500;
color: var(--muted);
line-height: 1.8;
}
.materials__list {
list-style: none;
margin: 0;
padding: 0;
}
.materials__item {
padding: 16px 0;
border-bottom: 1px solid var(--line);
font-size: 14px;
line-height: 1.6;
}
.materials__item:first-child {
padding-top: 0;
}
.knitting {
display: grid;
grid-template-columns: 1.1fr 1fr;
gap: 65px;
padding: 60px;
border-radius: 5px;
background: var(--ink);
color: var(--paper);
scroll-margin-top: 25px;
}
.knitting__title {
font-family: var(--serif);
font-size: clamp(38px, 4.1vw, 56px);
line-height: 1.13;
letter-spacing: -0.04em;
font-weight: 400;
margin: 0 0 25px;
}
.knitting__text {
font-size: 13px;
color: #c2c3b8;
line-height: 1.9;
}
.knitting__diagram {
align-self: center;
margin: 0;
color: #c1c0a9;
}
.knitting__drawing {
display: block;
width: 100%;
height: auto;
}
.knitting__caption {
font-size: 11px;
text-align: center;
}
.knitting__variables {
display: flex;
justify-content: space-between;
gap: 10px;
margin-top: 28px;
padding-top: 18px;
border-top: 1px solid #ffffff25;
color: #c2c3b8;
font-size: 10px;
}
.production {
padding-top: 100px;
scroll-margin-top: 25px;
}
.production__proof {
display: grid;
grid-template-columns: 1.2fr 1fr;
align-items: center;
gap: 50px;
padding: 42px 50px;
background: var(--wash);
border-radius: 4px;
}
.production__number {
font-family: var(--serif);
font-size: clamp(80px, 10vw, 140px);
line-height: 1;
letter-spacing: -0.065em;
margin: 0;
}
.production__unit {
display: block;
margin-top: 18px;
font-family: var(--sans);
font-size: 13px;
font-weight: 500;
letter-spacing: 0;
}
.production__label {
display: block;
font-size: 13px;
font-weight: 500;
line-height: 1.8;
}
.production__text {
margin: 20px 0 28px;
max-width: 320px;
color: var(--muted);
font-size: 14px;
line-height: 1.9;
}
.production__rule {
display: block;
height: 20px;
border-block: 1px solid var(--line);
background: repeating-linear-gradient(
to right,
transparent 0 9px,
#7c806b65 9px 10px
);
}
@media (max-width: 850px) {
.materials {
gap: 45px;
}
.knitting {
padding: 35px;
gap: 25px;
}
.production__proof {
padding: 35px;
}
}
@media (max-width: 600px) {
.applications {
padding-top: 45px;
}
.applications__summary {
gap: 15px;
}
.applications__text {
margin-left: 49px;
}
.materials {
grid-template-columns: 1fr;
padding: 60px 0;
gap: 30px;
}
.knitting {
grid-template-columns: 1fr;
padding: 35px 24px;
}
.production {
padding-top: 60px;
}
.production__proof {
grid-template-columns: 1fr;
gap: 32px;
padding: 30px 24px;
}
}
+173
View File
@@ -0,0 +1,173 @@
import { applications, yarns } from "../data";
import { Eyebrow, PageHero, SectionHeading } from "../components/Primitives";
import { ContactBanner } from "../components/Layout";
import CapacityValue from "../components/CapacityValue";
import Gallery from "../components/Gallery";
import "./Fabrics.css";
export default function Fabrics() {
return (
<>
<PageHero
label="Fabrics & capabilities"
title={
<>
Different yarns.
<br />
<em>Different structures.</em>
</>
}
>
<p>
RRS manufactures greige knitted fabrics across diverse yarns, fibres
and blends, with constructions developed according to the required
texture, appearance and apparel application.
</p>
</PageHero>
<section className="applications" id="applications">
<SectionHeading
label="Applications"
title={
<>
A construction for
<br />
<em>the application.</em>
</>
}
>
<p>
Explore some of the ways yarn and structure come together. These are
examples, not the limits of our manufacturing capability.
</p>
</SectionHeading>
<div className="applications__list">
{applications.map(({ title, text }, index) => (
<details
className="applications__item"
key={title}
open={index === 0}
>
<summary className="applications__summary">
<span className="applications__title">{title}</span>
<span className="applications__toggle" aria-hidden="true" />
</summary>
<p className="applications__text">{text}</p>
</details>
))}
</div>
</section>
<section className="materials">
<div className="materials__intro">
<Eyebrow>Materials & yarns</Eyebrow>
<h2 className="materials__title">
The structure starts
<br />
<em>with the yarn.</em>
</h2>
<p className="materials__text">
Different fabric designs require different yarn characteristics. RRS
manufactures fabrics using a wide variety of yarns and blends,
selected around the construction.
</p>
<span className="materials__note">
We manufacture the fabric, not the yarn.
</span>
</div>
<ul className="materials__list">
{yarns.map((yarn) => (
<li className="materials__item" key={yarn}>
{yarn}
</li>
))}
</ul>
</section>
<section className="knitting" id="circular-knitting" tabIndex={-1}>
<div className="knitting__copy">
<Eyebrow>Circular knitting</Eyebrow>
<h2 className="knitting__title">
One medium.
<br />
<em>Many constructions.</em>
</h2>
<p className="knitting__text">
Extensive circular-knitting capability allows RRS to change yarn,
composition, structure and machine configuration according to the
fabric being developed.
</p>
<p className="knitting__text">
The result is a broad manufacturing vocabulary ranging from
understated shirting constructions to highly textured knitted
surfaces.
</p>
</div>
<figure className="knitting__diagram">
<svg
className="knitting__drawing"
viewBox="0 0 420 350"
fill="none"
role="img"
aria-label="Conceptual circular knitting illustration, not a machine specification"
>
<ellipse cx="210" cy="90" rx="148" ry="50" stroke="currentColor" />
<ellipse cx="210" cy="90" rx="116" ry="33" stroke="currentColor" />
<path
d="M62 90v150c0 68 296 68 296 0V90M92 120v145m38-133v147m40-141v148m40-146v149m40-151v148m40-154v147m38-159v145"
stroke="currentColor"
strokeDasharray="3 4"
/>
<ellipse cx="210" cy="238" rx="148" ry="50" stroke="currentColor" />
<path
d="M210 9v38m0 241v45M30 163h30m300 0h30"
stroke="currentColor"
/>
<circle cx="210" cy="90" r="4" fill="currentColor" />
</svg>
<figcaption className="knitting__caption">
Circular knitting / Concept illustration
</figcaption>
<div className="knitting__variables">
<span>Yarn</span>
<span>Composition</span>
<span>Structure</span>
</div>
</figure>
</section>
<section className="production" id="production" tabIndex={-1}>
<SectionHeading
label="Production"
title={
<>
Developed
<br />
<em>to scale.</em>
</>
}
>
<p>
Development is only one part of the process. RRS is built to
reproduce successful constructions consistently across commercial
quantities.
</p>
</SectionHeading>
<div className="production__proof">
<p className="production__number">
<CapacityValue />
<span className="production__unit">tonnes / month</span>
</p>
<div className="production__detail">
<span className="production__label">
Approximate monthly production capacity
</span>
<p className="production__text">
Consistency across bulk orders remains one of our core
manufacturing priorities.
</p>
<span className="production__rule" aria-hidden="true" />
</div>
</div>
</section>
<Gallery />
<ContactBanner />
</>
);
}
+219
View File
@@ -0,0 +1,219 @@
.home-hero {
position: relative;
display: flex;
flex-direction: column;
justify-content: center;
overflow: hidden;
min-height: 590px;
margin-top: 24px;
border-radius: 7px;
color: var(--paper);
background: var(--ink);
}
.home-hero__image {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
object-fit: cover;
object-position: center 65%;
}
.home-hero__shade {
position: absolute;
inset: 0;
background: #1419108f;
}
.home-hero__content {
position: relative;
z-index: 1;
padding: 65px 62px 90px;
}
.home-hero__title {
margin: 38px 0 25px;
font-family: var(--serif);
font-size: clamp(54px, 6.7vw, 94px);
font-weight: 400;
line-height: 1.02;
letter-spacing: -0.055em;
}
.home-hero__description {
max-width: 400px;
margin: 0 0 30px;
color: #eeece5;
font-size: 15px;
line-height: 1.8;
}
.home-hero__actions {
display: flex;
flex-wrap: wrap;
gap: 12px;
}
.home-hero__bottom {
position: absolute;
bottom: 0;
left: 32px;
right: 32px;
display: flex;
justify-content: space-between;
gap: 20px;
padding: 20px 0;
border-top: 1px solid #ffffff35;
font-size: 11px;
line-height: 1.6;
}
.home-hero__tagline {
font-size: clamp(14px, 1.2vw, 16px);
font-weight: 500;
color: #ffffff;
letter-spacing: 0.01em;
}
.home-hero__image-note {
color: #e2e2dc;
}
.home-intro {
display: grid;
gap: 44px;
padding-top: 92px;
}
.home-intro__heading {
text-align: center;
}
.home-intro__body {
width: min(100%, 820px);
margin-inline: auto;
}
.home-intro__title {
margin: 0;
font-family: var(--serif);
font-size: clamp(36px, 4.2vw, 57px);
font-weight: 400;
line-height: 1.08;
letter-spacing: -0.045em;
}
.home-intro__lead {
font-family: var(--serif);
font-size: clamp(25px, 2.5vw, 34px);
line-height: 1.4;
letter-spacing: -0.015em;
margin: 0 0 24px;
}
.home-intro__text {
color: var(--muted);
font-size: 14px;
line-height: 1.9;
margin: 0 0 15px;
}
.home-intro__link {
display: inline-flex;
align-items: center;
gap: 30px;
margin-top: 12px;
padding-bottom: 7px;
border-bottom: 1px solid var(--ink);
color: var(--ink);
font-size: 11px;
text-decoration: none;
}
.process {
padding-top: 95px;
}
.process__list {
border-top: 1px solid var(--line);
}
.process__item {
display: grid;
grid-template-columns: minmax(0, 0.9fr) minmax(0, 1.1fr);
align-items: center;
gap: clamp(45px, 7vw, 120px);
padding: 58px 0;
border-bottom: 1px solid var(--line);
}
.process__item:nth-child(even) .process__visual {
order: -1;
}
.process__copy {
max-width: 560px;
padding-inline: clamp(0px, 2vw, 34px);
}
.process__verb {
margin: 0 0 24px;
font-size: 14px;
font-weight: 500;
color: var(--muted);
}
.process__title {
margin: 0 0 22px;
font-family: var(--serif);
font-size: clamp(35px, 3.3vw, 52px);
font-weight: 400;
line-height: 1.08;
letter-spacing: -0.04em;
}
.process__text {
margin: 0 0 28px;
color: var(--muted);
font-size: 14px;
line-height: 1.9;
}
.process__link {
display: inline-flex;
align-items: center;
gap: 24px;
padding-bottom: 7px;
border-bottom: 1px solid var(--ink);
color: var(--ink);
font-size: 12px;
text-decoration: none;
}
.process__visual {
width: 100%;
min-height: 320px;
aspect-ratio: 16 / 10;
}
.process__link:hover {
color: var(--accent);
}
@media (max-width: 760px) {
.home-hero {
margin-top: 18px;
min-height: 560px;
}
.home-hero__content {
padding: 42px 26px 130px;
}
.home-hero__title {
font-size: clamp(44px, 8.9vw, 68px);
margin-top: 32px;
}
.home-hero__bottom {
left: 26px;
right: 26px;
flex-direction: column;
gap: 8px;
padding: 15px 0;
}
.home-intro {
gap: 30px;
padding-top: 55px;
}
.process {
padding-top: 60px;
}
.process__item {
grid-template-columns: 1fr;
gap: 28px;
padding: 42px 0;
}
.process__visual,
.process__item:nth-child(even) .process__visual {
order: -1;
}
.process__copy {
max-width: none;
padding-inline: 0;
}
.process__visual {
min-height: 280px;
aspect-ratio: 4 / 3;
}
}
+137
View File
@@ -0,0 +1,137 @@
import { Link } from "react-router-dom";
import { capabilities, company } from "../data";
import {
Action,
Arrow,
Eyebrow,
SectionHeading,
Textile,
WhatsAppAction,
} from "../components/Primitives";
import { ContactBanner } from "../components/Layout";
import Gallery from "../components/Gallery";
import Stats from "../components/Stats";
import "./Home.css";
const processItems = [
{
...capabilities[0],
image: "fine",
href: "/fabrics#developments",
},
{
...capabilities[2],
image: "structured",
href: "/fabrics#circular-knitting",
},
{
...capabilities[1],
image: "natural",
href: "/fabrics#production",
},
];
export default function Home() {
return (
<>
<section className="home-hero">
<img
className="home-hero__image"
src="/images/knit-natural.jpg"
alt="Original illustrative macro knit texture, pending RRS photography"
width="1600"
height="1200"
fetchPriority="high"
/>
<div className="home-hero__shade" />
<div className="home-hero__content">
<Eyebrow>Circular knitted fabrics · RRS</Eyebrow>
<h1 className="home-hero__title">
Built to develop.
<br />
<em>Built to repeat.</em>
</h1>
<p className="home-hero__description">{company.description}</p>
<div className="home-hero__actions">
<Action to="/fabrics" variant="light">
Explore our fabrics
</Action>
<WhatsAppAction variant="ghost">Talk to RRS</WhatsAppAction>
</div>
</div>
<div className="home-hero__bottom">
<span className="home-hero__tagline">
Consistency at scale. Continuous development.
</span>
<span className="home-hero__image-note">
Illustrative texture · Photography to follow
</span>
</div>
</section>
<Gallery preview />
<section className="home-intro">
<div className="home-intro__heading">
<Eyebrow>Our fabrics</Eyebrow>
<h2 className="home-intro__title">
Greige is where
<br />
<em>it begins.</em>
</h2>
</div>
<div className="home-intro__body">
<p className="home-intro__lead">
RRS specialises in circular-knitted greige fabrics developed for
varied apparel applications.
</p>
<p className="home-intro__text">
Different constructions require different yarns, blends and machine
configurations. Our manufacturing capability allows us to develop
fabrics around structure, texture, composition and intended use.
</p>
<p className="home-intro__text">
From shirting constructions to textured and structured knits, we
work across polyester, cotton, rayon, stretch yarns and varied
blends.
</p>
<Link className="home-intro__link" to="/about">
Get to know RRS <Arrow diagonal />
</Link>
</div>
</section>
<Stats />
<section className="process">
<SectionHeading
label="How we work"
title={
<>
Behind the fabric:
<br />
<em>our process.</em>
</>
}
>
<p>
Development, circular knitting and repeatable bulk production work
as one discipline.
</p>
</SectionHeading>
<div className="process__list">
{processItems.map(({ title, subtitle, text, image, href }) => (
<article className="process__item" key={title}>
<div className="process__copy">
<p className="process__verb">{title}</p>
<h3 className="process__title">{subtitle}</h3>
<p className="process__text">{text}</p>
<Link className="process__link" to={href}>
Explore this capability <Arrow diagonal />
</Link>
</div>
<Textile className="process__visual" kind={image} />
</article>
))}
</div>
</section>
<ContactBanner />
</>
);
}
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 57 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 150 KiB

+265
View File
@@ -0,0 +1,265 @@
import { test, expect } from "@playwright/test";
import { whatsappUrl } from "../src/data.js";
const pages = [
["/", "Built to develop."],
["/fabrics", "Different yarns."],
["/about", "Built around"],
["/contact", "Lets talk"],
];
for (const width of [1440, 768, 390, 320]) {
for (const [path, heading] of pages) {
test(`${path} renders without errors at ${width}px`, async ({
page,
}, testInfo) => {
const errors = [];
page.on("pageerror", (error) => errors.push(error.message));
page.on("console", (message) => {
if (message.type() === "error") errors.push(message.text());
});
await page.setViewportSize({ width, height: 900 });
await page.goto(path);
await expect(page.getByRole("heading", { level: 1 })).toContainText(
heading,
);
await page.evaluate(() => document.fonts.ready);
for (const image of await page.locator("img").all()) {
await image.scrollIntoViewIfNeeded();
await expect(image).toHaveJSProperty("complete", true);
expect(
await image.evaluate((element) => element.naturalWidth),
).toBeGreaterThan(0);
}
await page.evaluate(() => window.scrollTo(0, 0));
await expect(page.locator("h1")).toHaveCount(1);
await expect(page.locator("[style]")).toHaveCount(0);
await expect(page.locator("form")).toHaveCount(0);
const metrics = await page.evaluate(() => ({
overflow: document.documentElement.scrollWidth > window.innerWidth,
unloadedImages: [...document.images].filter(
(image) => !image.complete || !image.naturalWidth,
).length,
fontsLoaded:
document.fonts.check('16px "Instrument Serif"') &&
document.fonts.check('16px "DM Sans"'),
contentWidthRatio:
document.querySelector("#main-content").getBoundingClientRect()
.width / window.innerWidth,
}));
expect(metrics).toMatchObject({
overflow: false,
unloadedImages: 0,
fontsLoaded: true,
});
expect(metrics.contentWidthRatio).toBeGreaterThan(0.85);
expect(errors).toEqual([]);
if (width === 1440 || width === 390) {
await page.screenshot({
path: testInfo.outputPath("page.png"),
fullPage: true,
});
}
});
}
}
test("desktop navigation, anchored CTA and browser back work", async ({
page,
}) => {
await page.goto("/");
await page
.getByRole("link", { name: "Explore our fabrics", exact: true })
.click();
await expect(page).toHaveURL("/fabrics");
await expect(
page
.getByRole("navigation", { name: "Main navigation" })
.getByRole("link", { name: "Fabrics", exact: true }),
).toHaveAttribute("aria-current", "page");
await page
.getByRole("link", { name: "Talk to us on WhatsApp", exact: true })
.click();
await expect(page).toHaveURL("/contact#whatsapp");
await expect(page.locator("#whatsapp")).toBeInViewport();
await expect(
page.getByRole("button", { name: "Talk to us on WhatsApp", exact: true }),
).toBeDisabled();
await expect(page.locator("#whatsapp-status")).toContainText(
"number to be supplied",
);
await page.goBack();
await expect(page).toHaveURL("/fabrics");
await page.goto("/contact#whatsapp");
await page
.getByRole("link", { name: "Talk to us on WhatsApp", exact: true })
.click();
await expect(page.locator("#whatsapp")).toBeInViewport();
});
test("mobile navigation closes on selection and Escape", async ({ page }) => {
await page.setViewportSize({ width: 390, height: 844 });
await page.goto("/");
const toggle = page.locator("#menu-toggle");
await toggle.click();
await expect(toggle).toHaveAttribute("aria-expanded", "true");
await page.keyboard.press("Escape");
await expect(toggle).toHaveAttribute("aria-expanded", "false");
await expect(toggle).toBeFocused();
await toggle.click();
await page
.getByRole("navigation", { name: "Main navigation" })
.getByRole("link", { name: "About", exact: true })
.click();
await expect(page).toHaveURL("/about");
await expect(toggle).toHaveAttribute("aria-expanded", "false");
await expect(page.locator(".owner")).toHaveCount(2);
await toggle.click();
await page.getByRole("link", { name: "RRS home", exact: true }).click();
await expect(page).toHaveURL("/");
await expect(toggle).toHaveAttribute("aria-expanded", "false");
});
test("application disclosures work from the keyboard", async ({ page }) => {
await page.goto("/fabrics");
const summary = page.locator("summary").filter({ hasText: "Textured knits" });
await summary.focus();
await page.keyboard.press("Enter");
await expect(
page.getByText(
"Knitted constructions where yarn and machine configuration are used to create distinctive surface character.",
),
).toBeVisible();
await page.keyboard.press("Enter");
await expect(
page.getByText(
"Knitted constructions where yarn and machine configuration are used to create distinctive surface character.",
),
).not.toBeVisible();
await expect(page.locator(".sample")).toHaveCount(7);
});
test("conversation starter copies with accessible feedback", async ({
page,
context,
}) => {
await context.grantPermissions(["clipboard-read", "clipboard-write"]);
await page.goto("/contact");
await page.getByRole("button", { name: "Copy conversation starter" }).click();
await expect(page.getByRole("status")).toHaveText("Message copied.");
expect(await page.evaluate(() => navigator.clipboard.readText())).toBe(
"Hello RRS, I'd like to discuss a fabric requirement.",
);
});
test("clipboard denial has a useful fallback", async ({ page }) => {
await page.goto("/contact");
await page.evaluate(() => {
Object.defineProperty(navigator, "clipboard", {
value: { writeText: () => Promise.reject(new Error("Denied")) },
configurable: true,
});
});
await page.getByRole("button", { name: "Copy conversation starter" }).click();
await expect(page.getByRole("status")).toContainText(
"Please select and copy",
);
});
test("WhatsApp requires a real international number and encodes the message", () => {
for (const value of ["", "[Number]", "123", "+12345678901", "not-a-number"])
expect(whatsappUrl(value)).toBeNull();
const url = new URL(whatsappUrl("12345678901"));
expect(url.origin).toBe("https://wa.me");
expect(url.searchParams.get("text")).toBe(
"Hello RRS, I'd like to discuss a fabric requirement.",
);
});
test("every page section unveils once when scrolled into view", async ({
page,
}) => {
for (const [path] of pages) {
await page.goto(path);
const sections = page.locator("#main-content > section");
expect(await sections.count()).toBeGreaterThan(0);
for (const section of await sections.all()) {
await section.scrollIntoViewIfNeeded();
await expect(section).toHaveClass(/reveal--visible/);
await expect(section).toHaveCSS("opacity", "1");
}
}
});
test("homepage feature headings are centered semantic h2 elements", async ({
page,
}) => {
await page.goto("/");
const fabricsHeading = page.getByRole("heading", {
level: 2,
name: "Greige is where it begins.",
});
const developmentsHeading = page.getByRole("heading", {
level: 2,
name: "Structure changes everything.",
});
await expect(fabricsHeading).toHaveCSS("text-align", "center");
await expect(developmentsHeading.locator("..").locator("..")).toHaveClass(
/section-heading--centered/,
);
});
test("capacity values roll in opposite directions and settle at 6070", async ({
page,
}) => {
for (const path of ["/", "/fabrics"]) {
await page.goto(path);
const capacity = page.getByLabel("60 to 70");
await expect(capacity).toHaveCount(1);
await capacity.scrollIntoViewIfNeeded();
await expect
.poll(() =>
capacity
.locator(".capacity-value__reel--up")
.evaluate((element) => element.getAnimations().length),
)
.toBeGreaterThan(0);
await expect(capacity.locator(".capacity-value__reel--up")).toHaveCSS(
"transform",
/matrix/,
);
await expect(capacity.locator(".capacity-value__reel--down")).toHaveCSS(
"transform",
/matrix/,
);
}
});
test("section unveiling respects reduced-motion preferences", async ({
page,
}) => {
await page.emulateMedia({ reducedMotion: "reduce" });
await page.goto("/");
const sections = page.locator("#main-content > section");
await expect(sections.first()).toHaveClass(/reveal--visible/);
for (const section of await sections.all()) {
await expect(section).toHaveCSS("opacity", "1");
await expect(section).toHaveCSS("transform", "none");
}
const capacity = page.getByLabel("60 to 70");
await capacity.scrollIntoViewIfNeeded();
expect(
await capacity
.locator(".capacity-value__reel--up")
.evaluate((element) => element.getAnimations().length),
).toBe(0);
});
test("unknown route provides a way home", async ({ page }) => {
await page.goto("/missing-page");
await expect(page.getByRole("heading", { level: 1 })).toHaveText(
"A loose thread.",
);
await page.getByRole("link", { name: "Back to home", exact: true }).click();
await expect(page).toHaveURL("/");
});
+12
View File
@@ -0,0 +1,12 @@
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
export default defineConfig({
plugins: [react()],
server: {
watch: {
// Avoid caching a temporarily empty module while an editor writes a file.
awaitWriteFinish: { stabilityThreshold: 200, pollInterval: 50 },
},
},
});