First CH Designs
BLK-14Blocks

採用セクション(手書き風アクセント)

やわらかいアンバーの色面ブロックと手書き風SVG(うねり下線・粗いリング・矢印)で「人の気配」を出す採用セクション。顔写真を使わずイニシャル円で人物を表し、募集要項の破線フレームと「募集要項を見る/社員インタビューを読む」の2導線を添える。画像素材ゼロ・外部依存ゼロで、モーションは transform/opacity のみ(参考: 保育・教育系の中途採用サイトに見られる色面+手書き装飾+顔のない人物図の構図をブランド再構築・2026-08-07)。

追加日:
2026-08-07
依存:
なし
tags
#recruit #block #hand-drawn #svg #no-image #no-dependency #motion #soft-color-field #reference:sakura-39-recruit.jp/career

プレビュー

BLK-14 — Recruit / 採用情報

いっしょに、
つくる人
探しています。

人数が少ないぶん、ひとつの仕事に関わる範囲が広くなります。 決まった役割をこなす人ではなく、決め方から一緒に考えられる人と働きたいと思っています。

  • K.M

    Web ディレクター

    在籍 3 年

    「言われたものを作る」で終わらせないでいい。最初にそう言われました。

  • S.T

    フロントエンド

    在籍 2 年

    実装の理由を毎回ことばにする。そのぶん、迷わなくなりました。

  • A.N

    デザイナー

    在籍 4 年

    案件の数より、直せる回数。手を動かす時間がちゃんとあります。

  • R.Y

    運用サポート

    在籍 1 年

    納品したあとの話を、いちばん長くしている会社だと思います。

Open positions / 募集要項

募集職種
Webディレクター / フロントエンドエンジニア / デザイナー
雇用形態
正社員・業務委託(いずれも試用期間3か月)
勤務地
フルリモート可(月1回の集まりは任意参加)
選考フロー
書類 → カジュアル面談 → 実務課題 → 最終面談
"use client";

import { useEffect, useRef, useState } from "react";
import { Montserrat } from "next/font/google";

const montserrat = Montserrat({
  weight: ["600", "700"],
  subsets: ["latin"],
  display: "swap",
});

/**
 * 採用セクション(やわらかい色面 × 手書き風アクセント)
 * 参考: 保育・教育系の中途採用サイト群に見られる「やわらかい色面ブロック+手書き風の線+顔のない人物図」の
 *       構図を、First CH のブランド(白基調+アンバー)へ再構築(コピーではない)。
 *
 * 汎用技法メモ(web-design-playbook 還流用):
 * - 手書き風の"描かれる"演出を stroke-dashoffset なしで作る: SVGを内包した span を origin-left で
 *   scaleX(0)→scaleX(1) させると、うねり線・下線・矢印は「左から引かれた」ように読める。
 *   dashoffset アニメーションと違い transform だけで済む(合成レイヤーのみで動くのでjank無し)。
 *   線が長いほど水平方向の歪みが出るので、うねりの振幅は小さめ・線幅は非スケールで持たせるのがコツ。
 * - 顔写真を使わずに「人の気配」を出す: イニシャル円(やわらかい色面)+手書き風の粗いリング。
 *   リングは真円ではなく制御点をわずかにずらしたベジェにし、hover で微回転させると"人が描いた線"に見える。
 *   写真の権利処理・差し替えコストがゼロで、採用ページの初期提案やプレースホルダにそのまま使える。
 * - 色面ブロックは blur-3xl のブロブを背景に置き、translateY の緩慢な往復(8〜11s)で"呼吸"させる。
 *   前景の罫線・文字は動かさないので可読性を落とさない。motion-reduce:animate-none で静止。
 * - 脱カード: 人物もポイントもカードで囲まず、hairline の縦罫(md 以上)と横罫でグルーピング。
 *   囲いを使うのは募集要項の破線フレーム1枚だけに絞り、「掲示物」の比喩として意味を持たせる。
 */

type Member = {
  initials: string;
  role: string;
  tenure: string;
  voice: string;
  /** イニシャル円のやわらかい色面 */
  field: string;
};

const members: Member[] = [
  {
    initials: "K.M",
    role: "Web ディレクター",
    tenure: "在籍 3 年",
    voice: "「言われたものを作る」で終わらせないでいい。最初にそう言われました。",
    field: "bg-amber-100",
  },
  {
    initials: "S.T",
    role: "フロントエンド",
    tenure: "在籍 2 年",
    voice: "実装の理由を毎回ことばにする。そのぶん、迷わなくなりました。",
    field: "bg-orange-100",
  },
  {
    initials: "A.N",
    role: "デザイナー",
    tenure: "在籍 4 年",
    voice: "案件の数より、直せる回数。手を動かす時間がちゃんとあります。",
    field: "bg-stone-200",
  },
  {
    initials: "R.Y",
    role: "運用サポート",
    tenure: "在籍 1 年",
    voice: "納品したあとの話を、いちばん長くしている会社だと思います。",
    field: "bg-amber-50",
  },
];

const outline: { term: string; detail: string }[] = [
  { term: "募集職種", detail: "Webディレクター / フロントエンドエンジニア / デザイナー" },
  { term: "雇用形態", detail: "正社員・業務委託(いずれも試用期間3か月)" },
  { term: "勤務地", detail: "フルリモート可(月1回の集まりは任意参加)" },
  { term: "選考フロー", detail: "書類 → カジュアル面談 → 実務課題 → 最終面談" },
];

/** 手書き風のうねり下線(origin-left の scaleX で"引かれる") */
function HandUnderline({ drawn }: { drawn: boolean }) {
  return (
    <span
      aria-hidden="true"
      className={`absolute -bottom-2 left-0 block w-full origin-left transition-transform duration-[900ms] ease-out motion-reduce:transition-none motion-reduce:scale-x-100 ${
        drawn ? "scale-x-100" : "scale-x-0"
      }`}
    >
      <svg
        viewBox="0 0 200 12"
        fill="none"
        preserveAspectRatio="none"
        className="block h-[9px] w-full text-amber-500"
      >
        <path
          d="M2 8C28 3 44 10.5 70 6.5 96 2.5 118 10 144 6 168 2.4 186 8.6 198 5.4"
          stroke="currentColor"
          strokeWidth="3"
          strokeLinecap="round"
          vectorEffect="non-scaling-stroke"
        />
      </svg>
    </span>
  );
}

/** 手書き風の粗いリング(真円にしない・hover で微回転) */
function HandRing() {
  return (
    <svg
      aria-hidden="true"
      viewBox="0 0 100 100"
      fill="none"
      className="pointer-events-none absolute -inset-[7px] h-[calc(100%+14px)] w-[calc(100%+14px)] text-amber-500/70 transition-transform duration-500 ease-out group-hover:rotate-6 motion-reduce:transition-none"
    >
      <path
        d="M51 4C73 6 95 23 95.5 48.5 96 74 75.5 96 49 95.5 24 95 4.5 74.5 4.5 49.5 4.5 25.5 24 5.5 47 4.2"
        stroke="currentColor"
        strokeWidth="1.6"
        strokeLinecap="round"
        vectorEffect="non-scaling-stroke"
      />
    </svg>
  );
}

/** 手書き風のきらめき(色面の余白に散らす小さな装飾) */
function Sparkle({ className }: { className: string }) {
  return (
    <svg
      aria-hidden="true"
      viewBox="0 0 24 24"
      fill="none"
      className={className}
    >
      <path
        d="M12 2.5c.6 5 3.9 8.3 9 9-5.1.7-8.4 4-9 9-.6-5-3.9-8.3-9-9 5.1-.7 8.4-4 9-9Z"
        fill="currentColor"
      />
    </svg>
  );
}

export default function RecruitTeaser() {
  const rootRef = useRef<HTMLElement>(null);
  const [active, setActive] = useState(false);

  // 画面内に入ったら reveal を開始(IO 非対応環境は即開始)
  // reduced-motion 対応は各要素の motion-reduce: variant で完結(JS判定は不要)
  useEffect(() => {
    const el = rootRef.current;
    if (!el) return;
    if (typeof IntersectionObserver === "undefined") {
      const id = requestAnimationFrame(() => setActive(true));
      return () => cancelAnimationFrame(id);
    }
    const io = new IntersectionObserver(
      (entries) => {
        if (entries.some((e) => e.isIntersecting)) {
          setActive(true);
          io.disconnect();
        }
      },
      { threshold: 0.15 },
    );
    io.observe(el);
    return () => io.disconnect();
  }, []);

  const reveal = `transition-[opacity,transform] duration-700 ease-out motion-reduce:transition-none ${
    active
      ? "translate-y-0 opacity-100"
      : "translate-y-6 opacity-0 motion-reduce:translate-y-0 motion-reduce:opacity-100"
  }`;

  return (
    <section
      ref={rootRef}
      aria-labelledby="recruit-teaser-heading"
      className="relative w-full max-w-4xl overflow-hidden bg-white px-1 py-2"
    >
      {/* やわらかい色面ブロック(緩慢に呼吸させる・前景は動かさない) */}
      <div aria-hidden="true" className="pointer-events-none absolute inset-0">
        <span className="absolute -top-16 -left-14 block h-72 w-72 rounded-full bg-amber-100 blur-3xl [animation:recruit-float_9s_ease-in-out_infinite_alternate] motion-reduce:animate-none" />
        <span className="absolute top-1/4 -right-16 block h-80 w-80 rounded-full bg-orange-100/80 blur-3xl [animation:recruit-float_11s_ease-in-out_1.2s_infinite_alternate] motion-reduce:animate-none" />
        <span className="absolute bottom-4 left-1/4 block h-64 w-64 rounded-full bg-stone-200/70 blur-3xl [animation:recruit-float_8s_ease-in-out_.6s_infinite_alternate] motion-reduce:animate-none" />
        <Sparkle className="absolute top-6 right-10 h-5 w-5 text-amber-400/80" />
        <Sparkle className="absolute top-24 right-[4.5rem] h-3 w-3 text-amber-300" />
      </div>

      <div className="relative">
        {/* ヘッダー */}
        <header className="max-w-2xl">
          <p
            style={{ transitionDelay: "0ms" }}
            className={`${montserrat.className} text-xs font-bold tracking-[0.25em] text-amber-600 uppercase ${reveal}`}
          >
            BLK-14 — Recruit / 採用情報
          </p>
          <h2
            id="recruit-teaser-heading"
            style={{ transitionDelay: "80ms" }}
            className={`mt-4 text-[clamp(1.7rem,4.2vw,2.7rem)] leading-[1.35] font-bold tracking-tight text-gray-900 ${reveal}`}
          >
            {/* 和文の改行は成り行きに任せず意図した位置で折る(行末に「す。」だけが残る事故を防ぐ) */}
            いっしょに、
            <br />
            <span className="relative inline-block whitespace-nowrap">
              つくる人
              <HandUnderline drawn={active} />
            </span>
            を
            <br className="sm:hidden" />
            探しています。
          </h2>
          <p
            style={{ transitionDelay: "160ms" }}
            className={`mt-6 text-[15px] leading-[1.9] font-medium text-gray-600 ${reveal}`}
          >
            人数が少ないぶん、ひとつの仕事に関わる範囲が広くなります。
            決まった役割をこなす人ではなく、決め方から一緒に考えられる人と働きたいと思っています。
          </p>
        </header>

        {/* はたらく人(顔写真を使わずイニシャル円+手書き風リングで気配を出す・脱カード) */}
        <ul
          role="list"
          className="mt-12 grid grid-cols-1 gap-y-8 border-t border-gray-200 pt-10 sm:grid-cols-2 sm:gap-x-8 md:grid-cols-4 md:gap-x-0 md:divide-x md:divide-gray-200"
        >
          {members.map((m, i) => (
            <li
              key={m.initials}
              style={{ transitionDelay: `${260 + i * 90}ms` }}
              className={`group md:px-5 md:first:pl-0 md:last:pr-0 ${reveal}`}
            >
              <span className="relative inline-block">
                <span
                  className={`flex h-[68px] w-[68px] items-center justify-center rounded-full ${m.field} transition-transform duration-500 ease-out group-hover:scale-105 motion-reduce:transition-none`}
                >
                  <span
                    className={`${montserrat.className} text-[15px] font-bold tracking-[0.08em] text-gray-700`}
                  >
                    {m.initials}
                  </span>
                </span>
                <HandRing />
              </span>
              <p className="mt-5 text-[15px] font-semibold text-gray-900">
                {m.role}
              </p>
              {/* 和文に英字向けの広いトラッキングを掛けると字間が割れるので、ここは既定の和文フォント・弱いトラッキングに留める */}
              <p className="mt-1 text-[11px] font-semibold tracking-[0.04em] text-amber-600 tabular-nums">
                {m.tenure}
              </p>
              <p className="mt-3 text-[13px] leading-[1.85] text-gray-600">
                {m.voice}
              </p>
            </li>
          ))}
        </ul>

        {/* 募集要項(唯一の"囲い"=掲示物の比喩。破線フレーム+やわらかい色面) */}
        <div
          style={{ transitionDelay: "640ms" }}
          className={`relative mt-14 rounded-[1.75rem] border-2 border-dashed border-amber-300/80 bg-amber-50/50 px-6 py-8 sm:px-9 sm:py-10 ${reveal}`}
        >
          <h3
            className={`${montserrat.className} text-[11px] font-bold tracking-[0.22em] text-amber-700 uppercase`}
          >
            Open positions / 募集要項
          </h3>
          <dl className="mt-6 divide-y divide-amber-200/70 border-t border-amber-200/70">
            {outline.map((row) => (
              <div
                key={row.term}
                className="grid grid-cols-1 gap-y-1 py-4 sm:grid-cols-[8.5rem_1fr] sm:gap-x-6"
              >
                <dt className="text-[13px] font-semibold text-gray-500">
                  {row.term}
                </dt>
                <dd className="text-[14px] leading-[1.8] font-medium text-gray-800">
                  {row.detail}
                </dd>
              </div>
            ))}
          </dl>
        </div>

        {/* 導線(募集要項=主 / インタビュー=従)+手書き風の矢印と添え書き */}
        <div
          style={{ transitionDelay: "760ms" }}
          className={`mt-10 flex flex-col items-start gap-5 sm:flex-row sm:items-center ${reveal}`}
        >
          <a
            href="#"
            className="group inline-flex items-center gap-3 rounded-full bg-amber-600 px-7 py-3.5 text-[15px] font-bold text-white transition-transform duration-300 ease-out hover:-translate-y-0.5 focus-visible:outline-2 focus-visible:outline-offset-4 focus-visible:outline-amber-600 motion-reduce:transition-none"
          >
            募集要項を見る
            <span
              aria-hidden="true"
              className="inline-block transition-transform duration-300 ease-out group-hover:translate-x-1 motion-reduce:transition-none"
            >
              →
            </span>
          </a>

          <a
            href="#"
            className="group inline-flex items-center gap-2 text-[15px] font-semibold text-gray-900 focus-visible:outline-2 focus-visible:outline-offset-4 focus-visible:outline-amber-600"
          >
            <span className="relative">
              社員インタビューを読む
              <span
                aria-hidden="true"
                className="absolute -bottom-1 left-0 h-px w-full origin-left scale-x-100 bg-gray-900 transition-transform duration-300 ease-out group-hover:scale-x-0 motion-reduce:transition-none"
              />
              <span
                aria-hidden="true"
                className="absolute -bottom-1 left-0 h-px w-full origin-right scale-x-0 bg-amber-600 transition-transform delay-150 duration-300 ease-out group-hover:origin-left group-hover:scale-x-100 motion-reduce:transition-none"
              />
            </span>
            <span
              aria-hidden="true"
              className="inline-block transition-transform duration-300 ease-out group-hover:translate-x-1 motion-reduce:transition-none"
            >
              →
            </span>
          </a>

          {/* 手書き風の矢印+添え書き(scaleX で引かれる) */}
          <p className="flex items-center gap-2 text-[13px] font-medium text-gray-500 sm:ml-auto">
            <span
              aria-hidden="true"
              className={`block w-14 origin-left transition-transform duration-[900ms] delay-300 ease-out motion-reduce:transition-none motion-reduce:scale-x-100 ${
                active ? "scale-x-100" : "scale-x-0"
              }`}
            >
              <svg
                viewBox="0 0 60 26"
                fill="none"
                preserveAspectRatio="none"
                className="block h-5 w-full text-amber-500 [transform:scaleX(-1)]"
              >
                <path
                  d="M3 20C13 21.5 30 18 40 8.5"
                  stroke="currentColor"
                  strokeWidth="2"
                  strokeLinecap="round"
                  vectorEffect="non-scaling-stroke"
                />
                <path
                  d="M30 6.5 41 7.5 39.5 18"
                  stroke="currentColor"
                  strokeWidth="2"
                  strokeLinecap="round"
                  strokeLinejoin="round"
                  vectorEffect="non-scaling-stroke"
                />
              </svg>
            </span>
            まずは話を聞くだけでも
          </p>
        </div>
      </div>

      {/* 色面ブロックの呼吸(transform のみ・reduced-motion で静止) */}
      <style>{`
        @keyframes recruit-float {
          from { transform: translate3d(0, 0, 0) scale(1); }
          to   { transform: translate3d(0, -18px, 0) scale(1.06); }
        }
      `}</style>
    </section>
  );
}

shadcn CLI でプロジェクトに追加

npx shadcn@latest add https://designs.first-ch.com/r/recruit-teaser.json